Now Loading ...
-
ViT - An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale
논문 정보
제목: An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale
arXiv: 2010.11929
Abstract
트랜스포머(Transformer) 아키텍처는 자연어처리(NLP) 분야에서 사실상 표준으로 자리잡았지만, 컴퓨터 비전(CV) 분야에서의 적용은 아직 제한적이다. (논문 시점 기준) 기존 연구들은 CNN과 attention을 함께 사용하거나 CNN의 일부 구성 요소만 attention으로 대체하는 정도였고, 전체적인 CNN 구조 자체는 그대로 유지하는 경우가 많았다.
본 논문은 이러한 CNN에 대한 의존이 반드시 필요한 것은 아니며, 이미지 패치(patch) 시퀀스에 순수한 트랜스포머를 직접 적용하는 것만으로도 이미지 분류 태스크에서 매우 좋은 성능을 얻을 수 있음을 보인다. 대량의 데이터로 사전학습(pre-training)한 뒤 ImageNet, CIFAR-100, VTAB 등 이미지 인식 벤치마크로 전이(transfer)했을 때, Vision Transformer(ViT)는 기존 SOTA CNN 대비 적은 연산 자원으로도 대등하거나 더 뛰어난 성능을 달성했다.
1. Introduction
NLP에서의 트랜스포머 확장 성공에 영감을 받아, 본 논문은 최소한의 구조 변경만으로 표준 트랜스포머를 이미지에 직접 적용한다. 이를 위해 이미지를 여러 개의 패치(patch)로 나누고, 이 패치들을 선형 임베딩(linear embedding)한 시퀀스를 트랜스포머의 입력으로 사용한다. 즉, 이미지 패치를 NLP의 토큰(단어)과 동일한 방식으로 취급하는 것이다. 모델은 지도학습(supervised) 방식의 이미지 분류로 학습된다.
ImageNet과 같은 중간 규모 데이터셋으로 강한 정규화(regularization) 없이 학습할 경우, ViT는 비슷한 크기의 ResNet보다 살짝 낮은 아쉬운 정확도를 보인다. 이는 어느 정도 예상 가능한 결과인데, 트랜스포머는 CNN이 태생적으로 갖고 있는 귀납적 편향(inductive bias) - 이동 등변성(translation equivariance)과 지역성(locality)을 갖고 있지 않기 때문에, 데이터 양이 충분하지 않으면 일반화 성능이 떨어지는 것으로 보인다.
그러나 더 큰 규모의 데이터셋(1,400만~3억 장)으로 학습하면 더 좋은 성능을 보여줬다.
Inductive Bias(귀납적 편향)
귀납적 편향이란, 모델이 학습 데이터에서 보지 못한 입력에 대해 일반화할 때 사용하는 ‘모델 구조에 미리 내장된 가정 또는 제약’을 말한다. CNN은 이미지에 특화된 두 가지 강한 귀납적 편향을 갖는다.
지역성(Locality): convolution 연산은 작은 커널(예: 3x3)로 이미지의 국소 영역만 보고 특징을 뽑는다. “의미 있는 패턴(엣지, 질감)은 가까운 픽셀들 사이에서 나온다”는 가정이 구조적으로 내장되어 있다.
이동 등변성(Translation Equivariance): 동일한 필터가 이미지 전체를 슬라이딩하며 적용되므로, 객체의 위치가 이동해도 그에 대응해 특징맵도 같은 방식으로 이동한다.
덕분에 CNN은 비교적 적은 데이터로도 이미지의 공간 구조를 잘 학습한다.
반면 ViT의 self-attention은 원칙적으로 모든 패치가 서로 전역적(global)으로 상호작용하고, 위치 정보도 학습 가능한 position embedding으로만 부여될 뿐 CNN처럼 구조적으로 강제되지 않는다. “가까운 패치끼리 더 관련 있다”거나 “패턴은 위치가 바뀌어도 동일하게 인식돼야 한다”는 가정을 모델이 미리 갖고 있지 않으므로, 트랜스포머는 이런 공간적 관계를 데이터로부터 처음부터 스스로 학습해야 한다.
2. Method
2.1. Vision Transformer (ViT)
트랜스포머는 원래 1차원 토큰 임베딩 시퀀스를 입력으로 받는다. 이미지(2D 데이터)를 트랜스포머에 넣기 위해, ViT는 이미지를 여러 개의 고정 크기의 패치로 나눈 뒤 각 패치를 1차원 벡터처럼 취급한다.
원본 이미지: $\mathbf{x} \in \mathbb{R}^{H \times W \times C}$
$H, W$: 이미지의 세로/가로 해상도
$C$: 채널 수 (RGB면 3)
이를 flatten된 2D 패치들의 시퀀스로 reshape:
\[\mathbf{x}_p \in \mathbb{R}^{N \times (P^2 \cdot C)}\]
$(P, P)$: 패치 하나의 해상도 (예: 16×16)
$N = HW / P^2$: 패치 개수 –> 이 값이 곧 트랜스포머의 시퀀스 길이(sequence length) 가 된다. (이미지가 $P \times P$ 크기의 “단어” $N$개로 쪼개지는 것)
패치 하나당 차원은 $P^2 \cdot C$ (패치를 펼쳐서 1차원 벡터로 만든 크기)
예를 들어 224×224×3 이미지를 16×16 패치로 나누면, $N = (224/16)^2 = 196$개의 패치가 생기고 패치 하나의 차원은 $16 \times 16 \times 3 = 768$이 된다.
Patch Embedding (Eq. 1)
트랜스포머는 모든 레이어에서 고정된 잠재 벡터 크기 $D$ 를 사용한다. 따라서 각 패치($P^2 \cdot C$ 차원)를 학습 가능한 선형 변환 $E$를 통해 $D$ 차원으로 매핑한다.
\[\mathbf{z}_0 = [\mathbf{x}_{\text{class}}; \; \mathbf{x}_p^1 \mathbf{E}; \; \mathbf{x}_p^2 \mathbf{E}; \; \cdots; \; \mathbf{x}_p^N \mathbf{E}] + \mathbf{E}_{pos}
\tag{1}\]
\[\mathbf{E} \in \mathbb{R}^{(P^2 \cdot C) \times D}, \qquad \mathbf{E}_{pos} \in \mathbb{R}^{(N+1) \times D}\]
$\mathbf{x}_p^i \mathbf{E}$: $i$번째 패치를 $D$차원으로 투영한 것. Patch embedding이라 부른다.
$\mathbf{x}_{\text{class}}$: 맨 앞에 붙이는 학습 가능한 class 토큰 (아래에서 설명)
$\mathbf{E}_{pos}$: 각 위치(패치)에 더해지는 position embedding. 시퀀스 길이가 $N+1$인 이유는 class 토큰까지 포함하기 때문.
결과적으로 $\mathbf{z}_0 \in \mathbb{R}^{(N+1) \times D}$ - 이 텐서가 트랜스포머 인코더의 입력이 된다.
Position Embedding
$\mathbf{E}_{pos}$는 D차원 벡터 하나가 아니라, $(N+1)$개의 행(row)을 가진 학습 가능한 룩업 테이블(lookup table) 이다. 행 하나하나가 각각 D차원 벡터이고, 몇 번째 위치인지(0번=class 토큰, 1번=첫 패치, 2번=두 번째 패치…)에 따라 해당 행이 선택되어 그 위치의 patch embedding에 더해진다.
\[\mathbf{z}_0^i = \mathbf{x}_p^i \mathbf{E} + \mathbf{E}_{pos}^{(i)}, \qquad i = 0, 1, \ldots, N\]
여기서 \(\mathbf{E}_{pos}^{(i)} \in \mathbb{R}^D\) 는 $\mathbf{E}_{pos}$ 행렬의 $i$ 번째 행이다. 즉 패치 196개짜리 시퀀스(class 토큰 포함 197개)라면, 서로 다른 197개의 D차원 벡터가 각각 학습되는 것이지, 모든 위치에 동일한 벡터가 더해지는 게 아니다.
기본 트랜스포머는 사인/코사인 함수로 만든 고정된(fixed) position encoding을 사용했다면, ViT는 이 값들을 랜덤 초기화 후 역전파로 학습시키는 것이다.
논문의 “1D”라는 표현은 벡터의 차원이 1차원이라는 뜻이 아니라, 패치들을 (row, col) 같은 2차원 좌표가 아닌 순서(1차원 인덱스, raster order)로만 구분한다는 뜻이다. 즉 14x14로 배열된 패치라도 위치를 2차원으로 인코딩하지 않고 1번, 2번, …, 196번처럼 일렬로 펴서 각각 독립적인 벡터를 학습시킨다.(2D-aware PE를 해도 큰 성능향상을 발견하지 못함. Appendix D.4)
여기까지 기억해야할 점은 patch embedding($\mathbf{E}$)과 position embedding($\mathbf{E}_{pos}$) 모두 완전히 학습되는 파라미터라는 것이다. 초기화 시점에는 위치 정보가 전혀 없고, 2D 공간 관계는 학습을 통해 처음부터 습득해야 한다.
Transformer Encoder (Eq. 2, 3, 4)
인코더는 Multi-head Self-Attention(MSA) 블록과 MLP 블록이 번갈아 쌓인 구조다. 각 블록 앞에는 LayerNorm(LN)이, 각 블록 뒤에는 residual connection이 적용된다.
\[\mathbf{z}'_\ell = \text{MSA}(\text{LN}(\mathbf{z}_{\ell-1})) + \mathbf{z}_{\ell-1}, \qquad \ell = 1 \ldots L
\tag{2}\]
\[\mathbf{z}_\ell = \text{MLP}(\text{LN}(\mathbf{z}'_\ell)) + \mathbf{z}'_\ell, \qquad \ell = 1 \ldots L
\tag{3}\]
\[\mathbf{y} = \text{LN}(\mathbf{z}_L^0)
\tag{4}\]
$L$: 인코더 레이어(블록) 개수
\(\mathbf{z}_{\ell-1}, \mathbf{z}_\ell \in \mathbb{R}^{(N+1) \times D}\) - 레이어를 통과해도 shape은 그대로 유지된다.
MLP는 GELU 비선형성을 사용하는 2개의 완전연결층으로 구성 (보통 중간에 차원을 확장했다가 다시 $D$로 축소, 예: $D \to 4D \to D$)
식 (4)에서 $\mathbf{z}_L^0$은 마지막 레이어 출력 시퀀스의 0번째 벡터, 즉 class 토큰 위치의 출력만 뽑아낸 것이다. 여기에 최종 LayerNorm을 적용한 것이 이미지 전체를 표현하는 벡터 $\mathbf{y}$가 된다.
[class] token
BERT의 [CLS] 토큰과 동일한 아이디어다. 패치 임베딩 시퀀스 $N$개만 있으면 “이미지 전체를 대표하는 하나의 벡터”가 없다. 그래서 학습 가능한 벡터 \(\mathbf{x}_{\text{class}}\)를 시퀀스 맨 앞에 인위적으로 추가한다 (\(\mathbf{z}_0^0 = \mathbf{x}_{\text{class}}\)).
이 토큰도 다른 패치 토큰들과 동일하게 self-attention에 참여한다. 즉 모든 레이어에서 다른 패치들의 정보를 attention을 통해 수집한다.
$L$개의 레이어를 다 통과하고 나면, 이 위치의 최종 출력 $\mathbf{z}_L^0$이 이미지 전체의 표현(representation) $\mathbf{y}$으로 사용된다 (식 4).
분류를 할 때는 이 $\mathbf{y}$에 classification head를 붙인다:
사전학습(pre-training) 시: hidden layer 1개짜리 MLP (tanh 활성함수)
미세조정(fine-tuning) 시: 단순한 linear layer 1개
요약
단계
연산
Shape
원본 이미지
—
$H \times W \times C$
패치로 분할
reshape
$N \times (P^2 \cdot C)$
Patch embedding
$\mathbf{x}_p \mathbf{E}$
$N \times D$
class 토큰 추가
concat
$(N+1) \times D$
Position embedding 추가
$+ \mathbf{E}_{pos}$
$(N+1) \times D$
인코더 통과 ($L$회 반복)
MSA + MLP
$(N+1) \times D$ (shape 불변)
최종 표현 추출
$\mathbf{z}_L^0$ 선택 + LN
$1 \times D$
분류
MLP / Linear head
$1 \times K$ (K = 클래스 수)
2.2. Fine-tuning and Higher Resolution
ViT는 일반적으로 대규모 데이터셋으로 사전학습(pre-training) 한 뒤, 이보다 작은 다운스트림 태스크로 미세조정(fine-tuning) 하는 2단계 방식을 따른다.
Classification Head 교체
사전학습 때 class 토큰 뒤에 붙어있던 예측 head(hidden layer 1개짜리 MLP)를 제거한다.
대신 0으로 초기화된 $D \times K$ 크기의 feedforward(linear) layer를 새로 붙인다.
$D$: hidden size (패치 임베딩 차원)
$K$: 다운스트림 태스크의 클래스 개수 (예: 사전학습은 ImageNet-21k의 21,000개 클래스였지만, 미세조정 대상이 CIFAR-10이면 $K=10$)
0으로 초기화하는 이유는, 학습 초반에 head의 출력이 무작위로 요동치지 않고 사전학습된 표현($\mathbf{y} = \text{LN}(\mathbf{z}_L^0)$)을 안정적으로 활용하며 서서히 적응하도록 하기 위함이다. (?)
더 높은 해상도로 파인튜닝
선행 연구(Touvron et al., 2019; Kolesnikov et al., 2020)에 따르면, 사전학습보다 더 높은 해상도의 이미지로 파인튜닝하면 성능이 향상되는 경우가 많다 (예: 224×224로 사전학습 후 384×384 또는 512×512로 파인튜닝).
이때 패치 크기 $P$는 그대로 유지한다. 해상도 $H, W$가 커지는데 $P$가 고정이면:
\(N = \frac{HW}{P^2}\)
값이 커진다. 즉 트랜스포머의 유효 시퀀스 길이 $N$이 사전학습 때보다 늘어난다. (?)
문제: 사전학습된 Position Embedding이 더 이상 맞지 않음
ViT는 구조적으로 임의의 시퀀스 길이를 처리할 수 있다(메모리가 허용되는 한). Self-attention의 가중치 파라미터($\mathbf{U}_{qkv}$ 등)는 시퀀스 길이 $N$에 의존하는 shape을 갖지 않기 때문이다.
다만 position embedding $\mathbf{E}_{pos} \in \mathbb{R}^{(N+1)\times D}$는 사전학습 때의 시퀀스 길이에 맞춰 학습된 룩업 테이블이다. 파인튜닝에서 시퀀스 길이가 늘어나면 이 테이블의 행 개수가 부족해지고, 설령 억지로 맞춘다 해도 그 의미(어느 위치를 표현하는지)가 더 이상 유효하지 않다.
해결책: Position Embedding의 2D 보간(Interpolation)
이를 해결하기 위해, 사전학습된 position embedding들을 원본 이미지에서의 위치(2D 좌표, row/col)를 기준으로 2D 보간하여 늘어난 시퀀스 길이에 맞는 새로운 position embedding 집합을 만들어낸다.
예: 기존에 14×14 (196개)로 학습된 position embedding을, 24×24 (576개)로 보간해서 늘리는 방식.
이는 사전학습에서 학습된 “위치 간 유사도 패턴”(가까운 패치일수록 비슷한 embedding을 갖는 경향, Appendix D.4/Figure 7)을 최대한 보존하면서 크기만 확장하는 방법이다.
핵심 포인트: 유일하게 수동 주입되는 귀납적 편향
이 해상도 조정과 패치 추출(patch extraction)이, ViT에 이미지의 2D 구조에 대한 귀납적 편향(inductive bias)이 수동으로 주입되는 유일한 지점이다.
ViT는 CNN과 달리 2D 공간 구조에 대한 가정을 모델 구조 자체에 내장하지 않고, 원칙적으로 모든 공간 관계를 데이터로부터 학습하도록 설계되었다. 그런데 예외적으로 딱 두 곳에서만 사람이 “이건 2차원 이미지다”라는 사전 지식을 명시적으로 반영한다.
패치 추출 (초기 시점): 이미지를 1차원으로 늘어놓지 않고 애초에 $P \times P$ 정사각형 패치로 잘라내는 것 자체가 “이미지는 2D 격자 구조를 갖는다”는 가정을 반영한 것.
Position embedding의 2D 보간 (미세조정 시점): 새로운 위치의 embedding 값을 채울 때, 단순히 1차원 순서로 나열하는 게 아니라 원본 이미지의 2D 좌표 관계를 기준으로 보간.
이 두 지점을 제외하면, self-attention 연산이나 position embedding 학습 등 나머지 모든 부분은 2D 구조에 대한 어떠한 가정도 없이 순수하게 데이터로부터 학습된다.
-
CS231n 9강. Object Detection, Image Segmentation, Visualizing
Youtube: Stanford CS231N 2025
강의자료
컴퓨터 비전 분야의 다양한 core task를 다룬다.
1. Semantic Segmentation
이미지 분할(Image Segmentation)의 하위 분류 중 하나로, 이미지 속의 각 픽셀이 무엇인지 파악하여 같은 종류의 객체는 같은 라벨로 묶어 분할한다.
학습 시에 training data의 image에는 각 픽셀에 대해 semantic category가 레이블되어 있다. Test 단계에서는 새로운 image의 각 pixel이 어떤 클래스인지 분류하는 문제가 된다.
각 픽셀에 대해 분류를 진행하지만 같은 클래스에 속하는 서로 다른 instance(객체)를 구분하지는 않는다. 즉, 사진 속에 소가 두 마리 있어도 둘 다 똑같이 “cow” 픽셀로만 칠해질 뿐 개체 단위로 나뉘지 않는다.
Semantic Segmentation의 가장 단순한 수행 방식은 Sliding Window이다. 일정 크기의 window를 이동시키며 그 안의 context를 이용해 중심 픽셀을 분류한다. 하지만 이 방식은 window 영역이 매번 겹치게 되는데도 겹치는 영역의 feature를 공유하여 재사용하지 않으므로 매우 비효율적이다.
이를 해결하기 위해 전체 이미지를 한 번에 합성곱 레이어에 통과시키는 방식을 생각할 수 있다. 다만 분류 문제와 달리 Semantic Segmentation의 output은 input과 같은 해상도여야 하므로, 해상도를 그대로 유지한 채 깊은 Conv 연산을 쌓으면 연산량과 메모리 요구량이 매우 커진다.
Fully Convolutional Network
이로 인해 제안된 구조가, 모든 레이어를 합성곱 연산으로 구성하면서 네트워크 내부에서 downsampling과 upsampling이 일어나는 Fully Convolutional Network(FCN)이다. 앞부분에서 해상도를 줄여 연산량을 아끼고 넓은 receptive field를 확보한 뒤, 뒷부분에서 다시 원본 해상도로 복원한다.
합성곱이 큰 이미지를 작은 특성맵으로 압축했다면, upsampling은 작은 특성맵을 다시 큰 크기로 복원하는 연산을 수행한다.
Upsampling에는 다양한 방법이 존재한다. 가장 기본적인 방법으로 Nearest Neighbor, Bed of Nails, Max Unpooling이 소개되며, 이들은 모두 학습 파라미터 없이 정해진 고정된 규칙으로 upsampling을 수행한다.
이와 달리 학습 가능한 Upsampling으로 Transposed Convolution이 있다. 일반 합성곱과 동일하게 filter가 학습되는 파라미터이며, stride $> 1$인 경우 출력 해상도가 입력보다 커진다.
U-Net
U-Net은 이름처럼 구조가 U자 형태를 띄고 있으며, 왼쪽의 수축 경로(contracting path)와 오른쪽의 확장 경로(expanding path)로 구성된다.
수축 경로에서 합성곱층으로 이미지를 점점 압축하고, 확장 경로에서 upsampling으로 다시 원본 크기로 복원한다는 점은 기존 FCN과 동일하다. U-Net의 핵심 아이디어는 Skip Connection으로, 수축 경로에서 만들어진 중간 특성맵을 따로 저장해두었다가 복원 단계에서 같은 해상도의 특성맵과 결합하여 사용한다.
압축 과정에서 손실되던 세밀한 경계 정보를 복원 단계에서 다시 활용할 수 있게 되므로, 더 정밀한 분할이 가능해진다.
2. Object Detection
객체 탐지는 말 그대로 사진이나 영상에 있는 객체를 찾아내는 기술이다. 각 객체가 무엇인지 분류하는 문제와 그 객체가 어디에 있는지 위치를 찾아내는 문제를 함께 포함한다.
객체 탐지는 1-stage 방식과 2-stage 방식으로 나뉘는데, 1-stage는 분류와 위치 검출을 한 번에 수행하는 방법이고, 2-stage는 후보 영역을 먼저 뽑고 그 후에 분류를 수행하는 방법이다.
이렇듯 객체 탐지는 클래스 분류와 위치를 찾기 위한 $(x, y, w, h)$ 회귀가 합쳐진 문제이므로, 손실 함수 또한 두 손실의 가중합으로 표현된다.
\[L = L_{\text{cls}} + \lambda L_{\text{loc}}\]
여기서 $\lambda$는 두 손실 사이의 균형을 맞추는 하이퍼파라미터이다.
Region Proposal
처음에는 이미지에 객체가 몇 개 있는지 알지 못하므로, CNN 연산을 이미지의 여러 부분에 대해 반복 수행해야 한다. 이때 CNN 연산을 수행할 후보 영역(Region of Interest, RoI)을 먼저 탐색해야 하며, 이러한 후보 영역을 찾는 대표적인 알고리즘이 Selective Search(선택적 탐색)이다.
입력으로 들어온 사진을 색상, 질감, 밝기와 같은 기준으로 매우 작은 영역들로 먼저 나눈 뒤, 나누어진 영역들을 색상, 질감, 인접성 등의 기준으로 비슷한 것끼리 병합한다.
이렇게 병합된 영역들을 기반으로 객체가 있을 것으로 판단되는 후보 영역(바운딩 박스)을 얻게 되고, 이 과정을 반복하여 다양한 크기의 후보 영역을 추출한다. (후보 영역의 개수는 하이퍼파라미터로, 아래에서는 2000개가 사용된다.)
R-CNN
R-CNN은 선택적 탐색으로 먼저 후보 영역을 구하고, 그 영역들을 각각 CNN의 입력으로 전달해 객체 탐지를 수행하는 기술이다. 이전의 객체 탐지 기술은 슬라이딩 윈도우 방식으로 이미지의 모든 영역을 검사했는데, R-CNN은 객체가 있을 만한 후보 영역을 먼저 구한 뒤 그것만 CNN에 입력한다는 점이 핵심이다.
추출한 RoI에 대해 클래스 분류와 Bbox Regression을 적용하여, 해당 영역의 레이블과 함께 바운딩 박스를 어디로 얼마나 이동·확대해야 하는지 조정값을 예측한다.
R-CNN의 단점은 다음과 같다.
합성곱층에 후보 영역을 넣기 위해 각 영역을 강제로 리사이즈하는데, 이 과정에서 이미지가 왜곡되어 원본의 형태 정보가 훼손될 수 있다.
2000개의 후보 영역을 각각 독립적으로 CNN에 통과시켜야 하므로 연산량이 매우 크다.
Spatial Pyramid Pooling (공간 피라미드 풀링, SPP)
SPP는 이를 해결하기 위해 처리 순서를 바꾼다. 이전과 같이 원본 사진에서 선택적 탐색으로 RoI 좌표 2000개를 저장해두되, CNN에 통과시키는 것은 후보 영역이 아니라 원본 이미지 전체이다. 이러면 특성맵 1개가 생성되는데, 여기에 앞서 구한 RoI 좌표를 대응시킨 뒤 완전연결층에 들어가기 직전에 크기를 통일시켜 분류를 수행한다.
이 방식은 크기 조정을 합성곱층 이전이 아니라 완전연결층 직전에 수행하므로 CNN이 왜곡이 덜한 특성맵을 얻을 수 있고, 2000번이 아니라 딱 1번만 CNN을 통과시키면 되므로 훨씬 빠르고 효율적이다.
Fast R-CNN
SPP의 아이디어로 R-CNN을 개선한 방식이 Fast R-CNN이다. “원본 이미지를 CNN에 한 번만 통과시킨다”는 핵심을 그대로 유지하면서, SPP층을 RoI Pooling이라는 더 단순하고 효율적인 방식으로 교체했다. 또한 분류기로 SVM 대신 softmax를 사용하고 bbox regression까지 하나의 네트워크에서 함께 학습하도록 통합했다.
RoI Pooling: 크기가 각각 다른 후보 영역을 풀링할 때, 영역을 목표 출력 크기만큼의 격자로 나눈 뒤 각 칸마다 max pooling을 적용해 결과 크기를 통일시키는 방법.
예시: $8 \times 8$ 특성맵 위의 $7 \times 5$ 크기 RoI를 $2 \times 2$ 형태의 고정된 출력으로 만들고자 하면, 가로 방향은 stride $= 3$, 세로 방향은 stride $= 2$를 적용해 $2 \times 2$ 크기의 결과를 얻을 수 있다.
Fast R-CNN에 와서는 CNN 연산이 더 이상 병목이 아니게 되었고, 신경망 바깥에서 CPU로 돌아가는 선택적 탐색이 전체 추론 시간의 대부분을 차지하는 새로운 병목이 되었다.
Faster R-CNN
Fast R-CNN에서 한 번 더 개선된 방식이다. 지금까지 본 R-CNN, Fast R-CNN은 모두 후보 영역을 추출할 때 선택적 탐색이라는 외부 알고리즘을 신경망 바깥에서 수행했는데, Faster R-CNN은 이것을 신경망 내부의 레이어로 대체한 구조이다.
이로써 CPU 대신 GPU에서 계산이 가능해져 속도가 크게 빨라졌고, 기존에는 후보 영역 추출과 객체 분류가 별개의 과정이어서 따로따로 학습해야 했다면 이제는 두 과정을 하나의 네트워크로 통합해 End-to-End 학습이 가능한 구조가 되었다.
Region Proposal Network (RPN)
Faster R-CNN에서 후보 영역을 추출하는 레이어이다.
RPN은 CNN을 통과해서 얻은 특성맵 위에서 동작한다. 슬라이딩 윈도우 방식으로 특성맵 전체를 탐색하며, 윈도우가 멈춘 위치에서 $N \times N$ 크기의 특성맵 값을 읽어 RPN 내부의 작은 신경망에 입력으로 사용한다.
이때 윈도우 크기가 $N \times N$으로 고정되어 있으면 사진 속 다양한 크기와 비율의 객체를 수용하기 어렵다는 문제가 생긴다.
이 문제를 해결하기 위해 도입한 것이 앵커(anchor)로, 앵커는 각 윈도우 위치를 중심으로 미리 정해둔 다양한 크기와 비율의 기준 박스이다.
원본 논문에서는 크기 3종류와 비율 3종류를 조합해 위치당 총 $3 \times 3 = 9$개의 앵커를 생성했고, 이 9개의 앵커가 특성맵의 모든 위치를 중심으로 생성된다.
따라서 특성맵 크기가 $W \times H$라면 전체 앵커 수는
\[W \times H \times 9\]
개가 된다. 이렇게 다양한 크기와 비율의 앵커를 이미지 전체에 배치해서 다양한 크기와 위치의 객체를 더 수월하게 인식할 수 있다.
RPN은 각 앵커마다 두 가지를 예측한다.
첫 번째는 해당 앵커 안에 객체가 있는지 없는지를 판단하는 이진 분류이다. 이때 학습 과정에서는 IoU(Intersection over Union)라는 기준을 사용하는데, IoU는 앵커 박스 $A$와 실제 정답 박스 $B$가 얼마나 겹치는지를 $0$에서 $1$ 사이의 값으로 나타낸 것이다.
\[\text{IoU}(A, B) = \frac{|A \cap B|}{|A \cup B|}\]
Faster R-CNN 논문에서는 $\text{IoU} \ge 0.7$인 앵커를 positive(객체), $\text{IoU} < 0.3$인 앵커를 negative(배경)로 라벨링하고 negative 앵커는 학습에서 제외한다.
두 번째는 바운딩 박스 회귀이다. 앵커는 규칙에 따라 미리 정해둔 박스이기 때문에 실제 객체의 위치/크기와 정확히 맞지 않는다. 바운딩 박스 회귀는 이 앵커를 실제 객체에 맞도록 미세 조정하는 과정이다. 이때 절대 좌표를 직접 예측하는 것이 아니라, 앵커 $(x_a, y_a, w_a, h_a)$ 대비 상대적인 보정값 $t = (t_x, t_y, t_w, t_h)$를 예측한다.
\[t_x = \frac{x - x_a}{w_a}, \quad
t_y = \frac{y - y_a}{h_a}, \quad
t_w = \log \frac{w}{w_a}, \quad
t_h = \log \frac{h}{h_a}\]
중심 좌표는 앵커의 크기로 나누어 정규화하고, 너비·높이는 비율에 로그를 취해 예측한다. 이렇게 하면 객체 크기에 관계없이 예측값의 스케일이 일정하게 유지되어 학습이 안정된다.
이진 분류에서 객체로 판단된 앵커에 한해서만 이 보정을 적용하고, 배경으로 판단된 앵커는 후보 영역이 될 수 없으므로 보정 없이 버려진다. RPN의 전체 손실 함수는 다음과 같다.
\[L(\{p_i\}, \{t_i\}) = \frac{1}{N_{\text{cls}}} \sum_i L_{\text{cls}}(p_i, p_i^{*}) + \lambda \frac{1}{N_{\text{reg}}} \sum_i p_i^{*} L_{\text{reg}}(t_i, t_i^{*})\]
여기서 $p_i$는 앵커 $i$가 객체일 예측 확률, $p_i^{\ast}$ 는 정답 라벨($0$ 또는 $1$)이다. 회귀 항에 $p_i^{*}$가 곱해져 있으므로 positive 앵커에서만 회귀 손실이 발생한다.
이렇게 모든 앵커에 대해 예측이 끝나면, 점수가 높은 순으로 정렬하고 NMS(Non-Maximum Suppression)로 중복된 박스를 제거한 뒤 상위 $n$개의 후보 영역을 추출한다. 이후는 Fast R-CNN과 동일하게 RoI Pooling을 거쳐 최종 클래스 분류와 박스 보정이 이루어진다.
결과적으로 Faster R-CNN은 하나의 네트워크 안에서 RPN의 이진 분류 + 회귀, 최종 다중 클래스 분류 + 회귀로 총 4개의 손실을 함께 학습하게 된다.
Single-Stage Object Detectors: YOLO / SSD / RetinaNet
분류와 위치 검출을 한 번에 수행하는 1-stage 객체 탐지 기술이다. 별도의 region proposal 단계 없이 특성맵의 각 위치에서 곧바로 클래스와 박스를 예측하므로 훨씬 빠르지만, 일반적으로 정확도에서는 2-stage 방식에 다소 밀린다. 이후 개별 포스트에서 따로 다룰 예정이다.
Object Detection with Transformers: DETR
DETR(DEtection TRansformer)은 객체 탐지를 집합 예측(set prediction) 문제로 재정의한 모델이다. 앵커 생성이나 NMS 같은 후처리 과정을 전부 제거한 것이 핵심이다.
CNN이 이미지에서 특성맵을 추출하고, 위치 정보를 담기 위해 positional encoding을 더한다.
이를 Transformer 인코더에 넣어 이미지 전역의 관계를 파악한 표현을 얻는다.
디코더에는 학습 가능한 $N$개의 object query를 입력한다. 각 query는 “이미지 어딘가에 있을 객체 하나”를 담당하는 슬롯 역할을 한다.
각 query의 출력은 작은 FFN을 거쳐 클래스 하나 + 박스 하나로 변환된다. 객체가 없으면 “no object($\varnothing$)” 클래스를 출력한다.
학습에서는 $N$개의 예측과 실제 정답 박스들 사이를 헝가리안 알고리즘(Hungarian matching)으로 1:1 최적 매칭한 뒤, 매칭된 쌍에 대해서만 분류·박스 손실을 계산한다.
\[\hat{\sigma} = \arg\min_{\sigma \in \mathfrak{S}_N} \sum_{i=1}^{N} \mathcal{L}_{\text{match}}(y_i, \hat{y}_{\sigma(i)})\]
이 1:1 매칭 덕분에 같은 객체에 여러 박스가 중복 예측되는 일이 구조적으로 억제되며, 그 결과 NMS가 필요 없어진다. 다만 수렴이 매우 느리고 작은 객체에 대한 성능이 떨어진다는 단점이 있어, 이후 속 연구로 개선된다.
3. Instance Segmentation
픽셀 단위 레이블 분류에 더해, 같은 클래스에 속하더라도 서로 다른 객체를 개별적으로 분리하는 task이다.
Object Detection이 박스 단위로 객체를 구분하고 Semantic Segmentation이 픽셀 단위로 클래스를 나눈다면, Instance Segmentation은 두 성질을 모두 갖는다고 볼 수 있다.
Mask R-CNN
한 줄로 요약하면 Faster R-CNN에 “마스크를 그리는 브랜치” 하나를 덧붙인 것이다.
Faster R-CNN은 각 후보 영역에 대해 두 가지를 예측했다.
이건 무슨 클래스인가? (분류)
박스를 어디로 옮겨야 하나? (회귀)
Mask R-CNN은 여기에 세 번째 문제를 추가한다.
이 박스 안에서 어느 픽셀이 객체이고 어느 픽셀이 배경인가?
박스 안을 색칠하는 작업이 하나 더 붙은 것뿐이고, 나머지는 Faster R-CNN 그대로다.
전체 흐름
위 그림을 왼쪽부터 따라가면 된다.
1) CNN + RPN
입력 이미지를 백본 CNN에 통과시켜 특성맵을 만들고, RPN이 객체가 있을 만한 후보 영역을 뽑는다. Faster R-CNN과 완전히 동일하다.
2) RoIAlign
후보 영역을 특성맵에서 잘라내 고정 크기로 맞춘다. 그림에서는 채널 $256$, 크기 $14 \times 14$가 된다. Fast R-CNN의 RoI Pooling 자리를 대체한 것으로, 뒤에서 따로 설명한다.
3) 두 갈래로 분기
위쪽 (기존 헤드): 클래스 점수 $C$개와, 클래스별 박스 좌표 $4 \times C$개를 예측한다. 좌표가 클래스마다 따로 있는 이유는 “개일 때의 최적 박스”와 “고양이일 때의 최적 박스”가 다를 수 있기 때문이다.
아래쪽 (마스크 헤드): $256 \times 14 \times 14$ 특성맵에 Conv를 몇 번 더 적용한 뒤, 마지막에 크기를 두 배로 키워 $C \times 28 \times 28$ 마스크를 출력한다.
여기서 중요한 건 마스크가 클래스 개수 $C$장만큼 나온다는 점이다. 즉 “개일 경우의 마스크”, “고양이일 경우의 마스크”를 전부 그려놓는다.
왜 마스크를 클래스마다 따로 그릴까?
마스크를 한 장만 그리게 하면 그 브랜치가 “이 픽셀은 객체인가?”와 “무슨 객체인가?”를 동시에 풀어야 한다. 반면 클래스마다 한 장씩 그리게 하면, 각 마스크는 모양(shape)만 신경 쓰면 되고 클래스 판단은 위쪽 분류 헤드에 맡길 수 있다.
그래서 학습할 때도 $C$장 전부가 아니라 정답 클래스에 해당하는 한 장에 대해서만 손실을 계산한다. 나머지 $C-1$장은 그냥 무시된다. 추론할 때도 분류 헤드가 “개”라고 답하면 $C$장 중 개 마스크만 골라 쓴다.
\[L_{\text{mask}} = \text{(정답 클래스 마스크의 픽셀별 이진 분류 손실 평균)}\]
각 픽셀은 softmax가 아니라 sigmoid로 0/1을 독립적으로 판단한다. 클래스끼리 경쟁시킬 필요가 없어졌기 때문이다. 논문은 이 분리가 성능 향상에 결정적이라고 보고했다.
RoIAlign: 반올림을 없앤 RoI Pooling
Mask R-CNN의 또 다른 핵심이다. 기존 RoI Pooling에는 반올림이 두 번 들어간다.
특성맵은 원본 이미지보다 해상도가 낮다. 예를 들어 원본 이미지를 $16$배 줄였다면, 원본의 $x = 100$은 특성맵에서 $100 \div 16 = 6.25$번째 칸에 해당한다. 그런데 특성맵에는 소수점 칸이 없으니 RoI Pooling은 이걸 $6$으로 잘라버린다. 그리고 이 영역을 여러 칸으로 나눌 때 또 한 번 반올림한다.
분류에서는 이 정도 오차가 별문제가 아니다. 박스가 조금 밀려도 “개”라는 답은 바뀌지 않는다. 하지만 마스크는 픽셀 단위로 정확히 맞춰야 하는 작업이라 치명적이다. 특성맵에서 한 칸 밀린 것이 원본 이미지에서는 16픽셀이 밀린 것이기 때문이다.
RoIAlign은 이 반올림을 전부 없앤다. $6.25$번째 위치를 그대로 두고, 주변 격자값들로 보간(bilinear interpolation)해서 그 지점의 값을 만들어낸다.
6번 칸과 7번 칸 사이 $0.25$ 지점의 값이 필요하면, 6번과 7번 값을 $3:1$로 섞어서 쓴다는 뜻이다.
RoI Pooling은 칸 안의 격자값 중 최댓값을 골랐지만, RoIAlign은 칸 경계가 소수라서 그럴 수가 없다. 대신 칸을 2×2로 나눠 각 조각의 중심에 점을 4개 찍고, 각 점의 값을 주변 격자로부터 보간해 만든 뒤 평균낸다.
손실 함수
세 가지 답을 하니 손실도 세 가지를 전부 더한다.
\[L = L_{\text{cls}} + L_{\text{box}} + L_{\text{mask}}\]
추론 과정
RPN이 후보 영역을 제안한다.
분류·회귀 헤드가 클래스와 보정된 박스를 예측하고, NMS로 겹치는 박스를 정리한다.
살아남은 박스에 대해서만 마스크 브랜치를 실행한다. (학습 때와 달리 RPN 제안이 아니라 보정이 끝난 최종 박스를 쓴다.)
예측된 클래스의 $28 \times 28$ 마스크를 골라 박스 크기로 확대하고, $0.5$를 기준으로 이진화한다.
확장: Human Pose Estimation
Human Pose Estimation은 사람 사진에서 어깨, 팔꿈치, 손목, 무릎 같은 관절의 위치를 찍어내는 작업이다. 찾아낸 점들을 순서대로 이으면 막대인간 형태의 뼈대가 만들어진다.
Mask R-CNN은 이 작업을 구조를 전혀 바꾸지 않고, 마스크 브랜치가 그리는 대상만 교체해서 수행한다.
핵심 아이디어: 관절 위치를 마스크로 표현한다
보통은 “팔꿈치가 $(x, y)$에 있다”를 좌표 회귀로 풀 것 같지만, Mask R-CNN은 그렇게 하지 않는다. 대신 정답을 격자에서 팔꿈치가 있는 칸만 $1$이고 나머지는 전부 $0$인 그림(one-hot 마스크)으로 만든다. 이렇게 하면 좌표를 맞히는 회귀 문제가 “어느 칸이 정답인지 고르는 분류 문제”로 바뀐다.
그래서 왼쪽 팔꿈치용 한 장, 오른쪽 무릎용 한 장… 이런 식으로 관절마다 한 장씩 총 $K$장을 출력한다. 마스크에서 클래스마다 한 장씩 그렸던 것과 완전히 같은 패턴이고, “클래스” 자리에 “관절”이 들어갔을 뿐이다.
마스크는 픽셀마다 독립적으로 판단하면 되지만, 관절은 정답 위치가 하나뿐이므로 칸들끼리 경쟁시켜야 한다. 그래서 마스크 한 장 안의 $m^2$개 픽셀 전체에 softmax를 적용한다.
마스크
키포인트
질문
이 픽셀이 객체인가?
관절이 어느 칸에 있는가?
정답 픽셀 수
여러 개
정확히 하나
출력 함수
픽셀별 독립 sigmoid
한 장 전체에 softmax
장 수
$C$장 (클래스 수)
$K$장 (관절 수)
추론 과정
각 장에서 값이 가장 높은 칸을 하나씩 고르면 관절 $K$개의 좌표가 나오고, 어깨–팔꿈치–손목처럼 미리 정해진 연결 규칙에 따라 이으면 뼈대가 완성된다. 정확한 위치 예측이 필요한 작업이라 마스크($28 \times 28$)보다 높은 $56 \times 56$ 해상도를 사용한다.
이처럼 Mask R-CNN은 하나의 구조로 detection, instance segmentation, keypoint detection을 모두 처리할 수 있다.
강의 후반부의 Visualization & Understanding 내용은 별도 포스트에서 다룬다.
-
CS231n 8강. Attention and Transformers
Youtube: Stanford CS231N 2025
강의자료
추가 참고자료: 위키독스: 딥 러닝 파이토치 교과서 - 입문부터 LLM 파인튜닝까지
1. Seq2Seq
Seq2Seq(Sequence-to-Sequence)은 크게 인코더(Encoder)와 디코더(Decoder)로 구성된 RNN 기반 아키텍처이다.
인코더는 현재 시점의 입력 $x_t$와 이전 시점의 은닉 상태 $h_{t-1}$을 받아 다음 은닉 상태 $h_t$를 계산한다.
\[h_t = f(x_t, h_{t-1})\]
이 은닉 상태 $h_t$는 시점 $t$까지의 입력 정보를 누적하여 담고 있는 벡터이다. 인코더의 마지막 시점의 은닉 상태를 컨텍스트 벡터(context vector) $c$라고 부르며, 이를 디코더의 첫 은닉 상태로 그대로 사용하거나 초기 은닉 상태를 계산하는 데 이용한다.
디코더는 이전 시점의 은닉 상태 $s_{t-1}$, 이전 시점의 예측값(출력 토큰) $y_{t-1}$, 그리고 컨텍스트 벡터 $c$를 이용하여 매 시점마다 예측값을 출력한다.
\[s_t = g(y_{t-1}, s_{t-1}, c)\]
슬라이드와 같이 디코더는 매 시점마다 단어를 하나씩 순차적으로 번역해나가는 방식으로 동작한다.
하지만 이러한 작동 방식에는 한계가 존재한다. 문장의 길이가 매우 길거나 입력이 여러 문장으로 이루어진 글일 경우, 모든 정보를 고정된 크기의 컨텍스트 벡터 $c$ 하나에 압축해서 담는 것은 매우 어렵다. 특히 입력 시퀀스의 앞쪽 정보일수록 인코더를 거치는 동안 점점 손실되는 장기 의존성 문제(long-term dependency problem)가 발생한다.
이 문제를 완화하기 위해 제안된 방식이 바로 Attention이다.
2. Attention
Attention은 디코더가 하나의 고정된 컨텍스트 벡터에만 의존하는 대신, 인코더의 모든 시점의 은닉 상태를 매 예측 시점마다 다시 참고할 수 있도록 하는 방식이다. 이때 모든 은닉 상태를 동일한 비율로 참고하는 것이 아니라, 해당 시점에 필요한 정보에 더 높은 비중을 두어 집중(attend)하여 참고한다.
작동 과정
인코더의 동작은 Seq2Seq과 동일하다고 가정하고, 디코더의 첫 은닉 상태 $s_0$이 있다고 가정하자.
먼저 Alignment Score(Attention Score라고도 불림)를 구한다. 이는 인코더의 각 은닉 상태가 디코더의 현재 시점 은닉 상태와 얼마나 유사한지를 나타내는 값이다. 인코더의 각 은닉 상태 $h_i$를 디코더의 은닉 상태 $s_0$과 내적하여 시점마다 스칼라 값을 구한다.
\[e_{1,i} = h_i \cdot s_0\]
이렇게 구한 스코어를 softmax에 통과시켜, 0~1 사이의 값을 가지며 합이 1인 확률 분포 형태의 Attention 가중치 $a_{1,i}$를 얻는다.
\[a_{1,i} = \frac{\exp(e_{1,i})}{\sum_j \exp(e_{1,j})}\]
이후 컨텍스트 벡터 $c_1$을 구한다. 모든 인코더 은닉 상태 $h_i$를 방금 구한 Attention 가중치 $a_{1,i}$로 가중합하여 구한다.
\[c_1 = \sum_i a_{1,i} h_i\]
이제 디코더는 이전 시점의 은닉 상태 $s_{t-1}$, 이전 시점의 예측값(출력 토큰) $y_{t-1}$, 그리고 현재 시점의 컨텍스트 벡터 $c_t$를 이용하여 매 시점마다 예측값을 출력한다. Seq2Seq과의 차이점은, 컨텍스트 벡터를 매 시점 새롭게 계산하여 사용한다는 점이다.
이 과정을 반복하면 매 시점 동일한 컨텍스트 벡터를 사용하는 대신, 입력에서 그때그때 연관된 정보를 참고하여 더 좋은 성능으로 예측을 수행할 수 있다. 이로써 입력 시퀀스 전체를 하나의 고정된 벡터로 압축해야 했던 bottleneck이 사라졌고, 매 시점마다 입력 시퀀스의 다른 부분에 더 집중할 수 있게 되면서 예측 성능이 향상되었다.
아래 슬라이드를 보면, 영어를 프랑스어로 번역하는 작업을 수행할 때 더 연관된 단어에 높은 가중치가 할당된 것을 시각적으로 확인할 수 있다.
이때 어떤 단어가 더 연관이 있고 어떤 정보를 더 참조해야 하는지는 인간이 직접 지시하는 것이 아니라, 모델이 역전파를 통해 직접 optimize되며 학습한 결과이다. 즉 위의 Attention 과정은 모두 미분 가능한(differentiable) 연산으로 구성되어 있다.
Query, Key, Value
위에서 살펴본 attention 과정을 조금 더 일반화된 형태로 표현하면, Query, Key, Value라는 세 종류의 벡터를 이용해 정리할 수 있다. 이 표현 방식은 이후 self-attention과 Transformer의 핵심 구성 요소가 되므로 중요하다.
앞서 구한 attention 과정을 다시 보면, 두 가지 역할이 섞여 있다는 것을 알 수 있다.
디코더의 은닉 상태 $s_{t-1}$은 “지금 무엇을 찾고 싶은가”에 대한 질의 역할을 한다. –> Query
인코더의 은닉 상태 $h_i$는 Query와의 유사도를 계산하는 데 사용되는 동시에(–> Key), 실제로 가중합되어 컨텍스트 벡터를 구성하는 데도 사용된다(–> Value).
즉, 위 방식에서는 $h_i$가 Key와 Value 역할을 동시에 수행하고 있었던 것이다. 이를 일반화하면, 하나의 입력 벡터로부터 서로 다른 선형 변환(Matrix Multiply)을 통해 Query, Key, Value를 각각 만들어낼 수 있다.
Attention Layer
\[Q = XW_Q, \quad K = XW_K, \quad V = XW_V\]
여기서 $X$는 입력 벡터(들의 집합), $W_Q, W_K, W_V$는 각각 Query, Key, Value를 만들기 위해 학습되는 가중치 행렬이다. Key와 Value를 분리함으로써 “어떤 기준으로 유사도를 비교할지”(Key)와 “실제로 어떤 정보를 가져올지”(Value)를 독립적으로 학습할 수 있게 된다.
행렬의 shape을 구체적으로 살펴보면 다음과 같다.
입력 시퀀스에 $N$개의 벡터(토큰)가 있고, 각 입력 벡터의 차원이 $D$라고 하자. 즉 $X$의 shape은 $(N, D)$이며, $X$의 각 행(row)이 하나의 토큰에 대응한다.
$W_Q, W_K$: shape $(D, D_Q)$ → $Q, K$의 shape은 $(N, D_Q)$
$W_V$: shape $(D, D_V)$ → $V$의 shape은 $(N, D_V)$
$Q$와 $K$는 이후 내적을 해야 하므로 반드시 같은 차원 $D_Q$를 가져야 한다. 반면 $V$는 내적 연산에 참여하지 않고 가중합에만 쓰이기 때문에 이론적으로는 $Q, K$와 다른 차원 $D_V$를 가져도 무방하다. 다만 실제 구현에서는 연산이 편리하도록 $D_Q = D_K = D_V$로 동일하게 설정하는 것이 일반적이다.
$Q$의 $i$번째 행: $i$번째 토큰이 “무엇을 찾고 싶은가”를 나타내는 쿼리 벡터
$K$의 $j$번째 행: $j$번째 토큰의 정보를 드러내는 키 벡터
$V$의 $j$번째 행: $j$번째 토큰이 실제로 “전달할 정보”를 담은 값 벡터
전체 계산 과정은 다음과 같이 정리된다.
1. 유사도 계산
Query와 모든 Key 사이의 유사도를 내적으로 구한다.
\[\text{score} = \frac{QK^T}{\sqrt{D_Q}}\]
$Q$의 shape은 $(N, D_Q)$, $K^T$의 shape은 $(D_Q, N)$이므로, $QK^T$의 shape은 $(N, N)$이 된다. 이 행렬의 $(i, j)$ 원소는 $i$번째 토큰의 Query와 $j$번째 토큰의 Key 사이의 유사도를 의미한다. 즉,
행(row) $i$: $i$번째 토큰(쿼리)이 나머지 모든 토큰(키)과 얼마나 유사한지를 담은 벡터
열(column) $j$: $j$번째 토큰(키)이 나머지 모든 토큰(쿼리)으로부터 얼마나 주목받는지를 담은 벡터
벡터의 차원 $D_Q$가 커질수록 내적 값의 분산이 커져 softmax의 gradient가 매우 작아지는 문제가 있어, 이를 방지하기 위해 $\sqrt{D_Q}$로 나누어 스케일링한다. 이를 Scaled Dot-Product Attention이라 한다.
2. 가중치 계산 (softmax)
\[A = \text{softmax}\left(\frac{QK^T}{\sqrt{D_Q}}\right)\]
여기서 핵심은 softmax를 행(row) 방향, 즉 axis=1로 적용한다는 점이다. $(N, N)$ 행렬에서 행 $i$는 “토큰 $i$가 쿼리일 때, 모든 토큰(키) $j = 1, \dots, N$에 대해 계산한 유사도”를 담고 있다. 이 행에 softmax를 적용하면, 하나의 쿼리 $i$가 나머지 모든 키들에 나눠주는 가중치의 합이 1이 되도록 정규화된다.
\[A_{i,j} = \frac{\exp(\text{score}_{i,j})}{\sum_{k=1}^{N} \exp(\text{score}_{i,k})}\]
만약 axis=0(열 방향)으로 softmax를 적용한다면 이는 “하나의 키가 모든 쿼리들로부터 받는 관심의 합이 1”이 되도록 정규화하는 것이 되어 의미가 완전히 달라진다. 우리가 원하는 것은 “각 쿼리가 자신의 관심을 여러 키에 어떻게 분배하는가”이므로, 반드시 행(row) 방향, axis=1로 계산해야 한다.
3. 출력 계산
이 가중치로 Value들을 가중합하여 최종 출력을 구한다.
\[\text{Attention}(Q, K, V) = AV = \text{softmax}\left(\frac{QK^T}{\sqrt{D_Q}}\right)V\]
$A$의 shape은 $(N, N)$, $V$의 shape은 $(N, D_V)$이므로 $AV$의 shape은 $(N, D_V)$가 된다. 즉 출력의 $i$번째 행은, $i$번째 토큰(쿼리)이 자신의 attention 가중치(행 $i$)로 모든 토큰의 Value 벡터를 가중합한 결과이다.
\[\text{output}_i = AV_i = \sum_{j=1}^{N} A_{i,j} V_j\]
여기서 $V_j$는 $V$의 $j$번째 행, 즉 $j$번째 토큰의 value 벡터 전체를 가리킨다 ($X, Q, K$와 마찬가지로 $V$도 “행 = 토큰”). $A$의 열 인덱스 $j$와 $V$의 행 인덱스 $j$가 정확히 같은 토큰을 가리키기 때문에, 별도의 transpose 없이 $AV$가 그대로 계산된다.
Note. $AV$를 실제로 계산할 때는 $D_V$개의 열에 대해 각각 $AV_{:,k} = \sum_j A_{i,j}V_{j,k}$ 형태로 (병렬로) 계산되는데, 열 하나만 떼어보면 “모든 토큰의 $k$번째 feature 값들”일 뿐 특정 토큰의 정보가 아니다. 하지만 모든 열에 걸쳐 동일한 가중치 $A_{i,j}$가 재사용되므로, $D_V$개 차원별 계산 결과를 이어붙이면 $\sum_j A_{i,j}V_j$, 즉 토큰 단위로 value 벡터 전체를 가중합한 것과 정확히 같아진다.
입력이 $N$개의 벡터였던 것처럼, 출력 역시 $N$개의 벡터(shape $(N, D_V)$)로, 입력 토큰 개수는 그대로 유지되면서 각 토큰의 표현이 attention을 통해 재구성된다.
이것이 기본적인 하나의 Attention Layer를 이룬다.
Cross-Attention
Cross-Attention은 글의 맨 처음에 다뤘던 Seq2Seq + Attention 구조와 동일한 구조로, 디코더의 은닉 상태 $s_{t-1}$이 Query로, 인코더의 은닉 상태들 $h_i$가 Key/Value로 사용되었던 과정을, Q/K/V 프레임워크로 다시 표현한 것이 바로 cross-attention이다. (Query와 Key&Value의 출처가 동일하지 않음)
\[Q = X_1 W_Q, \quad K = X_2 W_K, \quad V = X_2 W_V\]
Query: 정보를 요청하는 쪽의 $X_1$으로부터 계산
Key, Value: 정보를 제공하는 쪽의 $X_2$로부터 계산
Self-Attention
Self-Attention은 Query, Key, Value가 모두 동일한 입력으로부터 계산되는 경우이다.
\[Q = XW_Q, \quad K = XW_K, \quad V = XW_V\]
앞서 정리한 Attention Layer의 수식 자체는 동일하고, $Q, K, V$가 모두 같은 입력 $X$로부터 계산된다는 점이 핵심이다.
병렬 처리 가능 (RNN과의 결정적 차이): RNN 기반 Seq2Seq에서는 $h_t$를 구하기 위해 반드시 $h_{t-1}$이 먼저 계산되어야 했으므로, 시퀀스를 순차적으로만 처리할 수 있었다. 반면 self-attention은 $QK^T$ 연산이 하나의 행렬곱으로 이루어지므로, 시퀀스 내 모든 토큰에 대한 attention을 한 번에(병렬로) 계산할 수 있다.
Permutation Equivariant: self-attention 연산 자체는 입력 토큰의 순서를 구분할 수 있는 어떤 정보도 담고 있지 않다. 입력 토큰들의 순서를 뒤섞으면(permute), 출력 역시 동일하게 순서만 뒤섞인 채로 나온다. 하지만 자연어처럼 순서가 의미에 중요한 영향을 미치는 데이터를 다룰 때 이는 문제가 되므로, Transformer는 각 토큰에 positional encoding을 더해준다.
Masked Self-Attention: 디코더에서 self-attention을 사용할 경우, 아직 생성되지 않은 미래 시점의 토큰 정보를 미리 참고해버리면 문제가 된다. 이를 방지하기 위해 attention score 행렬에서 미래 위치에 해당하는 원소를 $-\infty$로 마스킹한 뒤 softmax를 적용하여, 해당 위치의 가중치가 0이 되도록 만든다. 이를 masked self-attention이라 하며, Transformer 디코더의 핵심 구성 요소이다.
Multi-Head Attention
지금까지 살펴본 attention layer는, 입력을 $D_Q$차원의 Q, K, V벡터로 선형변환해서 어텐션을 수행하였다. 그런데 문장 안에는 동시에 여러 종류의 관계가 존재한다(인접한 단어 관계, 주어-동사 관계, 의미적으로 먼 관계). Attention layer가 단 하나의 Q, K, V만 사용한다면, 이런 다양한 종류의 관계를 전부 하나의 유사도 계산에 넣어야 해서 표현력이 제한된다.
Multi-Head Attention은 하나의 큰 attention을 계산하는 대신 여러 개의 작은 attention을 독립적으로 병렬 계산한 뒤 합치는 방식으로 해결한다. 각각의 작은 attention을 head라고 부른다.
계산 과정
head의 개수를 $h$라 하자. $i$번째 head는 자신만의 가중치 행렬 $W_Q^{(i)}, W_K^{(i)}, W_V^{(i)}$를 가지고, 입력 $X$ (shape $(N, D)$)로부터 자신만의 $Q^{(i)}, K^{(i)}, V^{(i)}$를 계산한다.
\[Q^{(i)} = XW_Q^{(i)}, \quad K^{(i)} = XW_K^{(i)}, \quad V^{(i)} = XW_V^{(i)}\]
이때 각 head는 전체 차원 $D_Q$를 $h$등분한 $D_Q / h$ 차원을 가진다. 즉 $W_Q^{(i)}, W_K^{(i)}$의 shape은 $(D, D_Q/h)$, $W_V^{(i)}$의 shape은 $(D, D_V/h)$이며, 각 head의 $Q^{(i)}, K^{(i)}$는 shape $(N, D_Q/h)$, $V^{(i)}$는 shape $(N, D_V/h)$를 갖는다. (전체 $h$개 head의 차원을 모두 합치면 원래의 $D_Q$, $D_V$가 된다.)
각 head는 앞서 정의한 attention 연산을 독립적으로 그대로 수행한다.
\[\text{head}_i = \text{Attention}(Q^{(i)}, K^{(i)}, V^{(i)}) = \text{softmax}\left(\frac{Q^{(i)}{K^{(i)}}^T}{\sqrt{D_Q/h}}\right)V^{(i)}\]
이 $h$개의 head 출력을 마지막 축(feature 축)을 따라 이어붙이면(concatenate) shape $(N, D_V)$가 되고, 여기에 최종적으로 학습 가능한 행렬 $W_O$ (shape $(D_V, D_{out})$)를 곱해 최종 결과물을 얻는다.
\[\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)\,W_O\]
3. Transformer
Transformer는 (Multi-Head Self-Attention + MLP)로 구성된 하나의 블록을 반복해서 쌓아 만든 아키텍처이다. Attention이 토큰들 사이에서 정보를 주고받는 역할을 한다면, MLP는 각 토큰이 받은 정보를 개별적으로 가공하는 역할을 한다.
3.1 Transformer Block
하나의 Transformer block은 다음 네 가지 구성 요소로 이루어진다.
Multi-Head Self-Attention: 토큰들 사이에서 정보를 교환한다. (지금까지 다룬 유일하게 토큰 간 상호작용이 일어나는 연산이다.)
Residual Connection: 입력을 sub-layer의 출력에 더해준다.
Layer Normalization (Norm): 각 토큰 벡터를 정규화한다.
MLP (Feed-Forward Network): 각 토큰에 독립적으로 적용되는 두 개의 linear layer
이를 수식으로 정리하면 다음과 같다. 입력 $X$ (shape $(N, D)$)에 대해:
\[X' = \text{LayerNorm}\big(X + \text{MultiHeadSelfAttention}(X)\big)\]
\[Z = \text{LayerNorm}\big(X' + \text{MLP}(X')\big)\]
Residual Connection: Transformer block을 매우 깊게(수십~수백 층) 쌓다 보면, 역전파 과정에서 gradient가 소실되거나 최적화가 어려워지는 문제가 발생한다. Residual connection은 입력을 그대로 다음 층까지 우회 경로(shortcut)로 전달해, gradient가 손실 없이 앞쪽 층까지 흘러갈 수 있게 해준다.
Layer Normalization: 학습을 안정화하고 각 층의 입력 분포를 일정하게 유지하기 위함이다. 각 토큰 벡터 하나마다 feature 차원 $D$ 방향으로 평균과 분산을 구해 정규화한다. 즉 토큰 하나하나가 독립적으로 정규화되므로, 시퀀스 길이나 배치 크기에 영향받지 않는다. ((seq_len, embedding_dim) 행렬이 있으면, 각 행(row) - 즉 토큰 하나마다 embedding_dim 차원 방향으로 평균/분산을 구해서 정규화)
MLP (Feed-Forward Network): 시퀀스의 모든 위치(토큰)에 대해 동일한 가중치를 공유하는 두 개의 linear layer와 그 사이의 활성화 함수(ReLU 또는 GELU)로 구성되며, 보통 중간 차원을 원래보다 확장했다가 다시 원래 차원으로 축소한다.
\[\text{MLP}(x) = W_2\,\sigma(W_1 x + b_1) + b_2\]
MLP는 토큰 $i$의 벡터만 입력으로 받아 토큰 $i$의 벡터만 출력하므로, 토큰 간 정보 교환이 전혀 일어나지 않는다 –> 즉 Transformer block 하나는 “attention으로 정보를 모으고(토큰 간 정보교환) → MLP로 그 정보를 가공한다(연산)”는 두 단계를 반복하는 구조이다.
3.2 Positional Encoding
앞서 self-attention의 특징에서 다뤘듯, self-attention과 MLP는 모두 permutation equivariant하다. 입력 토큰의 순서를 뒤섞으면 출력도 그대로 순서만 뒤섞여 나올 뿐, 순서 정보 자체를 활용하지 못한다. 하지만 언어처럼 순서가 의미에 결정적인 데이터를 다루려면 각 토큰이 시퀀스 내에서 몇 번째 위치에 있는지를 모델에게 알려줘야 한다.
이를 위해 각 토큰의 임베딩에 위치 정보를 담은 Positional Encoding 벡터를 더해준다.
\[X_{\text{input}} = \text{Embedding}(\text{tokens}) + PE\]
원 논문(Attention is All You Need)에서는 서로 다른 주기를 갖는 sin, cos 함수를 이용한 sinusoidal positional encoding을 사용했다. 위치 $pos$, 차원 인덱스 $i$에 대해:
\[PE_{(pos,\, 2i)} = \sin\!\left(\frac{pos}{10000^{2i/D}}\right), \qquad PE_{(pos,\, 2i+1)} = \cos\!\left(\frac{pos}{10000^{2i/D}}\right)\]
이 방식의 한계점을 보완한 RoPE(Rotary Position Embedding) 방식이 이후 제안되었다.
3.3 Encoder-Decoder 구조
Transformer block을 여러 개 쌓아 Encoder와 Decoder를 구성하고, 이 둘을 연결하면 원 논문의 전체 Transformer 아키텍처가 된다.
Encoder: 입력 시퀀스 전체를 받아, Transformer block($=$ Self-Attention + MLP)을 $N$번 반복해서 쌓는다. 각 block을 지날 때마다 모든 토큰이 서로를 참고하며 문맥 정보를 더 풍부하게 담은 표현으로 갱신한다.
Decoder: 출력 시퀀스를 한 토큰씩 생성하며, 각 Transformer block마다 두 종류의 attention을 사용한다.
Masked Self-Attention: 지금까지 생성된 출력 토큰들끼리 self-attention을 수행하되, 미래 시점의 토큰은 볼 수 없도록 마스킹한다(masked self-attention).
Cross-Attention: Query는 디코더 자신의 표현에서, Key와 Value는 encoder의 최종 출력에서 가져온다. 디코더가 매 시점 인코더의 정보를 참고해 다음 토큰을 예측한다.
각 attention 다음에는 동일하게 residual connection, LayerNorm, MLP가 뒤따른다. 이러한 Decoder block을 $N$번 쌓은 뒤, 마지막에 linear layer와 softmax를 거쳐 다음 토큰에 대한 확률 분포를 출력한다.
3.4 RNN과의 비교
RNN (Seq2Seq)
Transformer
토큰 간 정보 전달 경로
순차적 ($O(N)$ 단계를 거쳐야 먼 토큰에 도달)
모든 토큰이 attention을 통해 1단계($O(1)$)만에 서로 직접 연결
학습 시 병렬화
불가능 (이전 시점의 은닉 상태가 있어야 다음 시점 계산 가능)
가능 (전체 시퀀스에 대한 attention을 행렬곱으로 한 번에 계산)
시간복잡도/메모리
시퀀스 길이 $N$에 대해 선형 ($O(N)$), 은닉 차원 $D$에 대해 $O(D^2)$
attention score 행렬이 $(N,N)$이므로 $N$에 대해 이차 ($O(N^2)$)
순서 정보
재귀 구조 자체에 순서가 내재됨
별도의 positional encoding으로 순서를 명시적으로 주입해야 함
-
CS231n 7강. Recurrent Neural Networks
Youtube: Stanford CS231N 2025
강의자료
1. Recurrent Neural Network
순환 신경망(RNN)은 입력과 출력을 시퀀스 단위로 처리하는 모델이다. 기존의 Feed-Forward 신경망이 고정된 크기의 입력만 받을 수 있었던 것과 달리, 가변적인 크기의 입력을 받을 수 있고, 데이터의 “순서(Order)”와 “시간적 흐름(Time-step)”을 처리할 수 있는 것이 가장 큰 특징이다.
아래 슬라이드는 RNN의 유형을 입력, 출력의 갯수에 따라 분류한 것이다.
다른 신경망들과 마찬가지로 입력에 대해 가중치(Weight) 연산을 거쳐 출력을 내는 것은 동일하지만, RNN의 가장 큰 차이점은 은닉 상태(hidden state)라는 내부 메모리를 가진다는 점이다.
은닉 상태 $h_t$는 RNN 셀이 다음 시점 $t+1$의 자신에게 보내는 값으로, 해당 시점까지의 과거 정보들을 압축한 벡터(Vector)이다. 이를 현재 시점의 입력값 $x_t$와 함께 사용하여 새로운 결과를 계산한다.
Vanilla RNN의 수식
먼저, RNN의 hidden state update를 점화식으로 표현하면 다음과 같다.
\[h_t = f_W(h_{t-1}, x_t)\]
함수 $f_W$는 파라미터(가중치) $W$를 가지며, 매 타임스텝마다 동일한 함수와 동일한 가중치가 재사용된다.
Hidden State가 아닌 실제 Output을 출력할때는 또 다른 가중치인 $W_{hy}$를 사용한다.
가장 기본적인 형태인 Vanilla RNN의 수식으로 전개하면 아래와 같다.
\[h_t = \tanh(W_{hh}h_{t-1} + W_{xh}x_t)\]
\[y_t = W_{hy}h_t\]
이전 상태의 정보 $h_{t-1}$와 현재의 입력 $x_t$가 각각의 가중치 행렬 $W_{hh}$, $W_{xh}$와 곱해지고, 이 두 값을 더한 뒤, 활성화 함수인( $\tanh$ )를 통과시켜 현재 시점의 은닉 상태 $h_t$를 업데이트한다.
Backpropagation through time (BPTT)
RNN은 시간 흐름에 따라 연산 과정을 펼쳐놓았을 때, 각 시점의 Loss를 이용하여 역전파를 통해 학습한다. 이처럼 시간의 흐름에 거슬러 올라가는 역전파를 Backpropagation Through Time (BPTT)라고 한다.
이후 설명은 Many to Many 유형을 기준으로 설명된다.
\[L = \sum_{t=1}^{T} L_t\]
모든 시점의 출력에 대한 손실을 구해 더한 전체 손실 $L$을 기반으로, 매 시점마다 공유되는 가중치 $W$의 그래디언트를 누적하여 업데이트한다.
이때 핵심은 “모든 시점에서 동일한 가중치 행렬 $W$($W_{hh}, W_{xh}, W_{hy}$)가 공유된다”는 점이다.
일반적인 깊은 신경망(DNN)은 층(Layer)마다 서로 다른 가중치를 가지지만, RNN은 시간에 따라 펼쳐진 각 단계마다 완전히 똑같은 가중치 $W$가 사용된 상태이다. 다변수 함수의 연쇄 법칙에 의해, 동일한 변수 $W$가 여러 결과($L_1, L_2, \dots, L_T$)에 영향을 미쳤다면, 전체 그래디언트는 각 손실이 $W$에 미치는 그래디언트를 모두 더한 값이 된다.
\[\frac{\partial L}{\partial W} = \sum_{t=1}^{T} \frac{\partial L_t}{\partial W}\]
나아가, 특정 시점 $t$의 손실 $L_t$가 가중치 $W$에 미치는 영향($\frac{\partial L_t}{\partial W}$)을 구하려면, 현재 시점 $t$뿐만 아니라 과거의 모든 시점($k=1, 2, \dots, t$)에서 $W$가 은닉 상태에 기여한 영향을 시간 역순으로 전부 거슬러 올라가며 곱하고 더해야 한다.
\[\frac{\partial L_t}{\partial W} = \sum_{k=1}^{t} \frac{\partial L_t}{\partial y_t} \frac{\partial y_t}{\partial h_t} \frac{\partial h_t}{\partial h_k} \frac{\partial h_k}{\partial W}\]
계산 및 업데이트 과정 요약:
시퀀스의 맨 마지막 시점 $T$부터 시작하여 과거 방향(왼쪽)으로 역전파를 진행.
각 시점 $t$에서 역전파되어 내려오는 그래디언트는 두 갈래로 나뉜다:
하나는 아래로 내려가 입력층의 가중치에 대한 그래디언트를 구하고,
다른 하나는 이전 시점 $t-1$의 은닉 상태($h_{t-1}$) 방향으로 흘러감
이렇게 시간의 역방향으로 흘러가는 그래디언트들이 각 시점의 가중치 $W$ 노드를 지나칠 때마다, 해당 위치에서 계산된 그래디언트 값들을 각각의 가중치 행렬( $dW_{hh}, dW_{xh}, dW_{hy}$ )에 계속해서 누적하여 저장.
시퀀스의 맨 처음($t=1$)까지 역전파가 모두 끝나면, 최종적으로 누적된 그래디언트를 이용해 가중치 $W$를 단 한 번 업데이트한다.
Truncated BPTT
이론적으로는 전체 시퀀스에 대해 Full BPTT를 수행하는 것이 맞지만, 실무적으로 시퀀스의 길이($T$)가 매우 길어지면(예: 수천 단어가 넘는 문장이나 긴 비디오) 다음과 같은 문제가 발생한다.
메모리 및 계산량 폭발: 역전파를 수행하려면 순전파 때 계산했던 모든 타임스텝의 은닉 상태($h_t$)와 활성화 값을 메모리에 유지해야 한다. 시퀀스가 길어질수록 필요한 메모리와 계산량이 증가.
기울기 소실/폭발 (Gradient Vanishing / Exploding): 동일한 가중치 행렬 $W$가 $T$만큼 반복해서 곱해지기 때문에, 역전파 과정에서 그래디언트가 사라지거나(Vanishing) 무한대로 커지는(Exploding) 문제가 발생.
이러한 대규모 시퀀스 데이터를 학습시키기 위한 현실적인 해결책으로 Truncated BPTT를 소개한다. 전체 시퀀스를 한 번에 처리하지 않고, 일정한 타임스텝 크기(t=10, 20…)로 쪼개어 학습하는 방식이다.
지정된 청크 크기만큼만 순전파를 진행하고, 해당 구간 내의 시점별 손실($L_t$)을 계산한다.
해당 청크 내에서만 역전파를 수행하여 가중치 $W$를 업데이트.
다음 청크 구간으로 넘어갈 때, 이전 청크의 마지막 은닉 상태 $h_t$ 값을 전달하여 다음 순전파의 초기값으로 사용한다.
다음 청크의 역전파를 계산할 때는 이전 청크로 그래디언트가 넘어가지 않도록 한다. (메모리를 절약하면서 과거의 정보 맥락을 유지)
위의 설명은 매 타임스텝마다 Loss가 발생하는 Many-to-Many를 가정한 것이고, Many-to-One 구조에서는 Loss가 마지막 청크의 최종 시점에만 존재한다.
따라서 마지막 출력층이 포함된 청크의 그래디언트만 계산하고 이전 청크는 아예 업데이트 하지 않거나(엄격한 Truncated BPTT), 출력층 청크의 가장 왼쪽 그래디언트만 메모리에 저장해두고 이전 청크에게 넘겨주는 방식을 적용할 수 있다.(Memory-Efficient BPTT)
Memory-Efficient BPTT
일부 중간 시점의 값만 캐시하여 완전히 그래디언트를 단절시키지 않는 방법을 Memory-Efficient BPTT라고 한다.
전체를 청크로 나누어 순전파를 진행할 때, 내부의 모든 연산 결과를 저장하지 않고 각 청크의 시작 지점의 은닉 상태(이전 청크에서 넘어온 최종 은닉 상태)만 메모리에 저장
마지막 출력층이 포함된 청크에서 최종 Loss를 기반으로 역전파를 수행할 때, 역전파가 해당 청크의 시작점(가장 왼쪽)에 도달하면, 그 위치의 그래디언트를 끊지 않고 저장해둠.
1번 과정에서 저장해 둔 은닉 상태 경계값을 초기값으로 삼아 해당 청크 구간만 순전파를 다시 한 번 계산. 이를 통해 지워졌던 활성화 값들을 복구한 뒤, 2번에서 기억해 둔 그래디언트를 이어받아 역전파를 마저 진행.
이 방식을 사용하면 역전파 시 순전파를 중복해서 다시 계산해야 하므로 연산 시간은 늘어나지만, 메모리 사용량을 단일 청크 크기만큼으로 고정시킨 채 Full BPTT의 효과를 얻을 수 있다.
2. LSTM (Long Short-Term Memory)
Vanilla RNN의 한계
앞서 말했듯이, Vanilla RNN은 시퀀스의 Time step이 길어질수록 과거의 정보가 뒤로 갈수록 점점 손실되는 장기 의존성 문제(Long-term dependency problem)를 겪는다.
역전파(Backpropagation) 과정에서 은닉 상태 $h_t$의 미분값을 구하려면 동일한 가중치 행렬 $W_{hh}$가 타임스텝의 수만큼 거듭제곱($W_{hh}^t$)되어 곱해진다. 이때 가중치 행렬의 가장 큰 특이값(Singular value)이 1보다 크면 기울기가 무한대로 치솟는 Gradient Exploding이, 1보다 작으면 0으로 수렴해버리는 Gradient Vanishing 현상이 발생한다. (보통 활성화 함수인 $\tanh$의 미분값이 1보다 작기 때문에 Vanishing 문제가 빈번하게 발생함.)
이러한 고질적인 Vanilla RNN의 한계를 극복하기 위해 제안된 구조가 LSTM(Long Short-Term Memory)이다.
LSTM(Long Short Term Memory)의 핵심 구조
LSTM은 기존 RNN의 은닉 상태 $h_t$ 외에도, 정보가 단절 없이 흐를 수 있는 셀 상태(Cell State, $c_t$)를 추가로 도입하고, 은닉 상태 또한 계산하는 방식이 조금 달라진다.
“은닉 상태 = 단기기억, 셀 상태 = 장기기억”
Vanilla RNN은 이전 은닉 상태 $h_{t-1}$와 현재 입력 $x_t$을 받아 $\tanh$ 함수를 거쳐 새로운 $h_t$를 만들었다면, LSTM은 이 두 값을 입력으로 받아 4개의 Gate를 통과시키며 정교하게 정보를 조작한다.
이전 은닉 상태 $h_{t-1}$과 현재 입력 $x_t$ 벡터에 각각 다른 가중치 행렬 $W$를 곱하여 4개의 게이트 값을 계산한다. 수식적으로는 표기의 간결함을 위해 두 벡터를 병합하여(Concat) 하나의 큰 가중치 행렬 $W$를 곱하는 것으로 표현한다.
\[\begin{pmatrix} i \\ f \\ o \\ g \end{pmatrix} = \begin{pmatrix} \sigma \\ \sigma \\ \sigma \\ \tanh \end{pmatrix} W \begin{pmatrix} h_{t-1} \\ x_t \end{pmatrix}\]
$i$ (Input Gate): 새로운 정보를 ‘얼마나(How much)’ 반영할지 결정하는 필터이다. 시그모이드($\sigma$)를 통과하여 0~1 사이의 값을 가지며, 클수록 정보를 더 받아들임.
$f$ (Forget Gate): 과거의 정보를 얼마나 잊을지 결정한다. 시그모이드($\sigma$)를 통과하므로 0~1 사이의 값을 가지며, 0에 가까울수록 과거 정보($c_{t-1}$)를 많이 지우고, 1에 가까울수록 정보를 보존.
$g$ (Gate Gate(?)): 셀 상태에 새롭게 기록하고 싶은 ‘정보의 내용물(What)’ 자체이다. $\tanh$를 통과하여 -1~1 사이의 값을 가지며, 어떤 방향(+ 또는 -)으로 상태를 변화시킬지 그 후보값을 나타냄.
$o$ (Output Gate): 업데이트된 최종 셀 상태($c_t$)를 바탕으로, 다음 은닉 상태($h_t$)로 얼마나 내보낼지 결정함.
State 업데이트
위에서 구한 4개의 Gate 값을 이용하여 LSTM은 두 가지 상태(Cell State와 Hidden State)를 업데이트한다. ($\odot$ 기호는 요소별 곱셈(Element-wise multiplication)을 의미.)
1) Cell State 업데이트 ($c_t$)
\[c_t = f \odot c_{t-1} + i \odot g\]
$f \odot c_{t-1}$ (과거 기억 필터링): 이전 셀 상태에 Forget Gate를 곱해 불필요한 과거 기억을 삭제.
$i \odot g$ (새로운 기억 융합): 현재 시점의 정보 내용물($g$)에 Input Gate($i$)의 반영 비율을 곱하여 필터링한 뒤, 그 결과를 과거 기억에 덧셈(+)으로 추가.
2) Hidden State 업데이트 ($h_t$)
\[h_t = o \odot \tanh(c_t)\]
새롭게 완성된 $c_t$를 $\tanh$에 통과시킨 뒤, Output Gate $o$와 곱해 최종적으로 외부(출력층 및 다음 시점)로 보낼 은닉 상태 $h_t$를 계산.
Do LSTMs solve the vanishing gradient problem?
핵심은 Cell State($c_t$)의 덧셈(+) 연산에 있다.
Vanilla RNN은 매 타임스텝마다 가중치 행렬 $W$를 ‘행렬 곱’으로 누적하기 때문에 그래디언트가 소실되었다. 하지만 LSTM의 $c_t$ 업데이트 식($c_t = f \odot c_{t-1} + i \odot g$)을 보면, $c_{t-1}$에서 $c_t$로 넘어갈 때 행렬 곱셈이 아닌 덧셈(+)을 통해 과거 정보를 이어받는다.
덧셈 연산은 역전파 시 그래디언트를 1 그대로 흘려보내주는 역할을 한다. 즉, Forget Gate $f$가 1에 가까운 값을 유지해 준다면, 아무리 먼 과거의 정보라도 방해를 받지 않고 전달될 수 있다. 이는 ResNet의 Skip Connection과 유사한 성질이다.
-
CS231n 6강. CNN Architectures
Youtube: Stanford CS231N 2025
강의자료
1. Components of CNNs
이전 강의에서 CNN의 핵심인 Convolution Layer와, Pooling Layer에 대해 살펴보았었다. 이외에도 CNN을 이루는 몇가지 Layer들을 소개한다.
Normalization Layer
Normalization Layer(정규화 계층)은 Input data의 값 분포를 최적의 분포(Optimal Distribution)로 정규화하기 위해 학습하는 계층이다. 기본적으로 표준정규화를 통해 $N(0, 1)$의 분포를 따르게 만든 후 학습한 파라미터에 따라 Scale/Shift를 진행한다.
이렇게 정규화를 하면 다음과 같은 효과를 얻을 수 있다:
최적화가 안정화된다: 각 레이어로 들어가는 입력의 분포가 일정하게 유지되어 Loss의 변동도 안정됨. 학습 과정이 훨씬 안정적으로 진행된다.
더 큰 Learning Rate를 사용할 수 있다: 그래디언트의 스케일이 안정적으로 유지되어, 정규화 없이는 발산했을 큰 학습률로도 수렴할 수 있다.
그래디언트 소실/폭주가 완화된다: 데이터 분포가 활성화 함수의 saturating 구간(예: sigmoid, tanh의 양 끝)으로 치우치는 것을 막아, 역전파 시 그래디언트가 안정적으로 전달된다.
약한 규제(regularization) 효과가 있다: 특히 BatchNorm처럼 미니배치 단위로 통계를 구하는 경우, 배치마다 통계값이 조금씩 달라지는 노이즈가 더해져 일종의 regularization처럼 작동.
대부분의 정규화 계층은 이 표준정규화 방식을 사용하며, 각 종류별 차이점은 이 data를 정규화할 통계적 수치를 구하는 방식이다. 해당 강의에서는 LayerNorm이 대표적으로 소개되었다.
정육면체에서 세 축은 각각 batch($N$), channel($C$), spatial($H, W$)을 의미하고, 색칠된 영역이 하나의 평균/분산을 공유하는 단위다. 즉 정규화 종류별 차이는 “이 색칠 영역을 어떻게 잡느냐”의 차이다.
Batch Norm
각 채널마다 배치 전체와 공간 차원 $(N, H, W)$에 걸쳐 통계를 계산한다. 정육면체로 보면 채널 축의 한 칸을 고정하고 N · H,W 방향으로 펼쳐진 영역이 하나의 정규화 단위가 된다.
배치 통계에 의존하므로 배치 크기에 민감하다. 배치가 작으면 통계가 불안정해져 성능이 떨어진다.
학습과 추론의 동작이 다르다. 학습 중에는 미니배치 통계를 사용하면서 running mean/variance를 EMA로 누적하고, 추론 시에는 이 누적값을 사용한다.
CNN에서 표준적으로 사용된다.
:: EMA(Exponential Moving Average - 지수이동평균)
\[\mu_{\text{running}} \leftarrow \alpha \cdot \mu_{\text{running}} + (1-\alpha)\cdot \mu_B\]
\[\sigma_{\text{running}}^2 \leftarrow \alpha \cdot \sigma_{\text{running}}^2 + (1-\alpha)\cdot \sigma_B^2\]
Layer Norm
각 샘플마다 채널과 공간 차원 $(C, H, W)$ 전체에 걸쳐 통계를 계산한다. 배치 방향으로는 전혀 섞이지 않으므로 배치 크기에 독립적이고, 학습·추론 동작이 동일하다.
배치 통계를 쓰지 않아 배치가 작거나 1이어도 잘 동작한다.
RNN, Transformer처럼 시퀀스 길이가 가변적이고 배치 의존성을 피하고 싶은 구조에서 주로 사용된다.
Instance Norm
각 샘플의 각 채널마다 공간 차원 $(H, W)$에 대해서만 통계를 계산한다.
Group Norm
채널을 $G$개의 그룹으로 나눈 뒤, 각 샘플의 그룹마다 (그룹 내 채널들 + 공간)에 걸쳐 통계를 계산한다.
$G=1$이면 LayerNorm, $G=C$(채널 수)이면 InstanceNorm과 같아지는, 둘 사이를 일반화한 형태다.
Regularization: Dropout
각 순전파(forward pass)마다 레이어의 뉴런 출력(활성값) 일부를 랜덤하게 0으로 만든다. 각 뉴런을 비활성화할 확률 $p$는 하이퍼파라미터이며, 보통 0.5 정도를 사용한다.
만약 특정 뉴런들이 항상 함께 켜져 어떤 특징을 “분담”해서 학습하면, 그 조합에 과하게 의존하게 된다. 랜덤하게 뉴런을 꺼버리면 어떤 뉴런이 사라질지 모르므로, 각 뉴런은 서로 의존하지 않고 그 자체로 유용한 특징을 중복해서 학습할 수 있다.
추론 시에는 뉴런을 끄지 않고 전부 사용한다. 그런데 학습 때는 평균적으로 $(1-p)$ 비율의 뉴런만 켜져 있었는데, 추론 때는 전부 켜지므로 다음 레이어로 들어가는 입력의 기댓값(스케일)이 학습 때보다 커진다.
이 차이를 맞춰주기 위해 스케일을 보정하는 과정이 필요하다.
Activation Functions
각 층의 출력에 비선형성을 추가해주는 역할을 하며, 일반적으로 신경망은 선형 층과 활성화 함수를 번갈아 쌓는 구조로 이루어진다.
Sigmoid
입력값을 $[0,1]$ 범위의 값으로 압축한다. 이전에는 인기가 많았으나 양 끝쪽의 gradient가 0에 가까워 중첩되어 사용될 경우 역전파에서 기울기가 소실되는 Vanishing Gradient문제로 인해 오늘날 잘 사용되지 않음. 다만 출력을 확률처럼 $[0, 1]$로 해석할 수 있어, 이진 분류의 마지막 출력층으로는 여전히 사용된다.
ReLU
$f(x) = max(0,x)$ 로 표현되어 굉장히 계산이 단순하고 양수 범위에서는 기울기 소실 문제가 완화되었다. 실제로 sigmoid/tanh보다 수렴도 빠르다.
다만 $X<0$의 음수에서 기울기가 죽는 Dead Cell이 되어 버리는 점과, 출력에 음수가 없어, sigmoid와 마찬가지로 zero-centered가 아니다.
이러한 단점을 완화한 변형으로 음수 구간에 작은 기울기를 부여하는 Leaky ReLU, Transformer 계열에서 표준적으로 쓰이는 GELU 등이 있으며, 그래도 대부분의 경우 기본 ReLU로 충분하다.
2. CNN Architectures
VGGNet, ResNet 등 대표적인 CNN 구조들이 간략히 소개되었다. 이 글에서는 7x7 conv 한 번 대신 3x3 conv를 세 번 쌓는 것이 더 효율적이라는 점만 짚고 넘어간다. (둘은 동일한 receptive field를 갖지만, 3x3을 쌓는 쪽이 파라미터 수는 더 적고 비선형성(ReLU)이 더 많이 들어간다.)
ResNet에 대한 내용은 다른 글에서 이미 다루었다: ResNet 포스트
3. Build and Train CNNs
모델을 성공적으로 학습시키기 위한 다양한 전략을 소개한다.
Weight Initialization
가중치의 초기값은 학습 과정에서 굉장히 중요한 요소이다. 학습이 시작될 때 각 층을 거친 활성값(activation)의 분포가 어떻게 변하는지를 좌우하기 때문이다.
먼저, 모든 가중치를 0이나 동일한 값으로 초기화하면 안 된다. 모든 뉴런이 완전히 똑같은 출력과 똑같은 gradient를 갖게 되어 학습 내내 같은 값으로만 업데이트되기 때문이다. 그래서 가중치는 랜덤하게 초기화하되, 스케일을 잘 잡아야 한다.
너무 작으면: 층을 거칠수록 활성값이 점점 줄어 0으로 수렴한다. 활성값이 0이면 그에 의존하는 gradient도 0이 되어, 깊은 층일수록 학습이 되지 않는다.
너무 크면: 활성값이 점점 커지며, sigmoid·tanh 같은 함수는 양 끝 구간으로 몰려 gradient가 소실되고, 그렇지 않은 경우엔 값이 발산하며 Gradient Exploding이 일어난다.
Xavier Initialization
층의 입력 차원 $D_{in}$을 이용해 가중치의 분산을 $\frac{1}{D_{in}}$로 맞추는 방식이다. 즉 표준정규분포에서 뽑은 값에 $\sqrt{\frac{1}{D_{in}}}$를 곱한다. 입력 뉴런이 많을수록 가중치를 작게 만든다.
Kaiming Initialization
위 방식을 ReLU에 맞춰 보정한 방식이다. ReLU는 입력의 음수 절반을 0으로 죽여버리므로 출력의 분산이 대략 절반으로 줄어드는데, 이 손실을 보상하기 위해 분산에 2를 곱한 $\frac{2}{D_{in}}$을 사용하여 스케일한다. ReLU 계열을 쓰는 현대 CNN에서 표준적으로 사용된다.
Data Preprocessing
학습 데이터 전체에서 구한 채널별 평균과 표준편차를 이용해, 입력 이미지를 평균 0, 표준편차 1에 가깝게 정규화하는 것이 일반적이다. 데이터를 zero-centered하게 만들어두면 학습 초기의 최적화가 안정적으로 진행된다. 이때 사용한 통계값(평균·표준편차)은 검증·테스트 데이터에도 동일하게 적용한다.
Data augmentation
데이터 증강이라 불리며, 훈련 데이터를 변형하여 데이터의 크기와 다양성을 늘리는 기법이다. 좌우 반전, 크기 조절, 색상 변형, 일부 자르기(crop) 등으로 데이터를 변형시켜 학습시킨다. 모델이 이런 변형에 불변(invariant)하도록 유도해 일반화 성능을 높이는 효과를 갖는다. 변형은 훈련 시에만 적용한다.
Transfer Learning
학습시키려는 데이터가 너무 적거나 컴퓨팅 자원이 충분하지 않을 때, 사전 훈련된(pre-trained) 모델을 활용하는 방법이다. 보통 CNN모델의 앞쪽 레이어는 edge·texture 같은 범용적인 특징을 학습하므로 다른 task에도 그대로 재사용할 수 있다는 점을 이용한다.
Feature Extractor: 기존 모델의 CNN 레이어들은 동결(freeze)하고, 분류층만 새 데이터에 맞게 훈련한다. 데이터가 매우 적을 때 적합하다.
Fine-Tuning: 분류층뿐 아니라 기존 CNN 레이어의 일부(또는 전체)도 함께 학습시킨다. 데이터가 어느 정도 확보됐을 때 사용하며, 보통 사전 학습된 가중치가 망가지지 않도록 작은 learning rate로 학습한다.
-
CS231n 5강. Image Classification with CNNs
Youtube: Stanford CS231N 2025
강의자료
딥러닝 아키텍처가 주목받기 이전에는, 이미지 분류와 같은 대부분의 task에서 Feature Extraction을 사람이 직접 수행하였다. 분류에 중요하다고 판단되는 feature를 직접 정의하는 코드를 작성하고, 이를 벡터와 같은 수치 배열로 변환한 뒤 Linear Classifier 등의 분류기에 입력으로 사용하는 방식이었다.
반면 오늘날의 딥러닝 아키텍처는, 이러한 Feature Extraction 과정조차 모델이 학습을 통해 스스로 수행한다. 이 방식이 사람이 직접 feature를 설계하는 것보다 더 정확할 뿐 아니라, 사람이 미처 포착하지 못한 feature까지 찾아낼 수 있다는 것이 핵심 아이디어다.
실제로 이 아이디어는 옳았고, 지난 십여 년간 End-to-End Learning 기반의 Neural Network가 다양한 분야에서 뛰어난 성능을 보여줬다. 이번 강의에서는 이미지 분류 task에서 이러한 feature 학습을 가능하게 하는 CNN(Convolutional Neural Network) 구조에 대해 다룬다.
1. CNN: Convolutional Neural Network
앞선 강의에서 이미지를 모델의 입력으로 사용할 때, 이를 1차원 벡터로 flatten하는 과정에서 이미지의 공간적 구조(spatial structure)가 손실되는 문제가 있었다. CNN은 이 문제를 해결하면서 이미지 분류에서 훨씬 뛰어난 성능을 보이는 아키텍처이다.
CNN 아키텍처는 기본적으로 합성곱층(Convolutional Layer) 과 풀링층(Pooling Layer), 그리고 활성화 함수가 번갈아 쌓이는 구조로 이루어진다.
Fully Connected Layer와 Template Matching
Fully Connected Layer에서의 dot product는 본질적으로 template matching이다. 두 벡터가 같은 방향을 가리킬수록 내적값이 커지고, 직교하면 0에 가까워지기 때문이다.
Fully Connected Layer의 핵심을 정리하면 다음과 같다:
가중치 행렬은 일종의 template의 집합(bank of templates) 이다.
각 template은 입력과 동일한 크기(size)를 가진다.
출력은 각 template과 입력 간의 matching score이다.
Convolutional Layer
CNN은 이러한 template matching과 filter bank의 개념을 Convolutional Layer로 일반화한다. Fully Connected Layer와의 핵심적인 차이는 두 가지다:
필터가 입력 size와 같을 필요가 없다.
이미지를 flatten하지 않는다. 더이상 이미지를 펼치지 않고 입력 이미지의 공간적 구조 (C × H × W) 를 그대로 유지한다.
필터(filter, 혹은 kernel) 는 작은 크기의 sub-image(가중치 텐서)로, 입력 이미지 전체를 슬라이딩 윈도우(sliding window) 방식으로 이동하며 각 위치에서 dot product를 수행한다. 이때 필터의 채널 수는 입력 이미지의 채널 수와 반드시 일치해야 한다.
이러한 필터와 이미지의 부분 내적을 모든 위치에 대해 반복하면 2D activation map이 생성되며, 이 map의 각 값은 이미지의 해당 위치가 필터 패턴과 얼마나 일치하는지를 나타낸다.
필터를 여러 개 사용하면 각 필터마다 하나의 activation map이 생성될 것이고, 이를 쌓으면 최종적으로 (필터 수 × H’ × W’) 크기의 출력 텐서가 만들어진다. 이때 필터의 수가 곧 출력의 채널 수가 된다.
정리하면, Convolutional Layer의 출력은 activation map이며, 학습을 통해 조정되는 값은 필터의 가중치이다. CNN은 이러한 합성곱층(선형 연산) 과 활성화 함수 또는 풀링층(비선형 연산) 이 번갈아 쌓이는 구조이다.
What do Conv filters learn?
필터가 직접 경사하강법을 통해 학습한 결과는 이미지의 특징을 추출할 수 있는 값들일 것이다. 실제로 AlexNet의 첫번째 layer의 filter를 확인해보면 다음과 같다.
이미지를 학습한 대부분의 CNN에서 앞쪽 레이어는 특정 방향의 edge, 색상 대비와 같은 저수준(low-level) 특징을 학습하고, 깊은 레이어로 갈수록 이러한 저수준 특징들이 조합되어 텍스처, 패턴, 물체의 일부나 전체와 같은 고수준(high-level) 특징을 학습하게 된다.
이렇게 모델이 데이터를 단순한 특징에서 점점 추상적인 특징을 학습하는 과정을 계층적 특징 학습(hierarchical feature learning)이라고 하며, CNN이 이러한 특성을 보이는 것은 각 필터의 수용 영역(receptive field)의 크기가 점점 넓어지기 때문이다.
Receptive Field
Receptive Field란 activation map의 한 픽셀이 영향을 받는 원본 입력 이미지의 영역을 말한다.
예를 들어 필터 크기가 3×3이라면, 1번째 레이어 activation map의 한 픽셀은 원본 이미지의 3x3의 정보를 담고 있을 것이다. 이때 그 다음 레이어는 이 activation map 위에서 또 3×3을 보는데, 해당 map이 이미 원본의 3x3을 담고 있으니 2번째 activaion map의 한 픽셀은 원본 이미지의 5×5 영역에 영향을 받는 것이다.
이렇게 레이어가 쌓일수록 receptive field는 점점 커져, $L$번째 레이어의 receptive field 크기는 대략 $1 + L*(필터 크기-1)$로 증가한다.
\[\text{Receptive Field Size} = 1 + L \times (K - 1)\]
결국 깊은 레이어의 뉴런일수록 원본 이미지의 더 넓은 영역을 종합해서 하나의 값을 만들어내기 때문에, 자연스럽게 지역적인 특징보다 더 추상적인 특징을 학습하는 것이다.
한편, 필터들이 서로 다른 특징으로 수렴할 수 있는 이유는 학습 시작 시 필터가 서로 다른 값으로 랜덤 초기화되기 때문이다. 만약 모든 필터가 동일한 값으로 초기화된다면, 역전파 시 동일한 gradient를 받아 결국 같은 특징만을 학습하는 대칭 문제(Symmetry Problem) 가 발생한다.
Stride, Padding
Stride: 필터가 이미지(데이터) 위를 이동할 때의 이동 간격이다.
Padding: 이미지의 가장자리를 특정 값(0)으로 채우는 것이며, 보통 합성곱 연산 후에도 이미지의 크기를 유지하고 싶을때 사용된다.
입력 크기 $W$, 필터 크기 $K$, 패딩 $P$, 스트라이드 $S$가 주어졌을 때, 출력 크기 $W’$는 다음과 같다:
\[W' = \frac{W - K + 2P}{S} + 1\]
예를 들어 입력이 7×7, 필터가 3×3, Padding=0, Stride=1이면:
\[W' = \frac{7 - 3 + 0}{1} + 1 = 5\]
출력은 5×5가 된다. Padding=1로 설정하면:
\[W' = \frac{7 - 3 + 2}{1} + 1 = 7\]
입력과 동일한 7×7 크기가 유지된다.
Pooling
activation map의 크기를 줄이는데 stride를 크게하는 방법 외에도 Pooling을 사용한다. 합성곱과 비슷하게 kernel을 map위에서 슬라이딩하는데, 합성곱 대신 풀링의 종류에 따라 값을 계산한다.
대표적인 풀링 방식은 두 가지다:
Max Pooling: kernel 영역 내의 최댓값을 취한다. 영역에서 가장 강한 신호만을 남김.
Average Pooling: kernel 영역 내의 평균값을 취한다.
아래는 kernel 크기 2×2, stride=2로 Max Pooling을 수행하는 예시로, 출력 크기가 절반으로 줄어드는 것을 확인할 수 있다.
주의할 점은 Average Pooling은 합성곱과 마찬가지로 선형(linear) 연산인 반면, Max Pooling은 최댓값을 선택하는 비선형(non-linear) 연산이라는 점이다. 이 때문에 Max Pooling은 별도의 활성화 함수 없이도 비선형성을 추가하는 효과가 있다.
Translation Equivariance
Equivariance(등변성)은 입력에 변환을 가했을 때 출력도 동일한 변환이 적용되는 성질이다.
\[f(T(x))=T(f(x))\]
입력 x를 먼저 변환하고 함수 f를 적용한 결과와, 함수 f를 먼저 적용하고 변환한 결과가 같다. 합성곱 연산은 같은 필터가 이미지의 모든 위치에 동일하게 적용(weight sharing)되기 때문에 사진처럼 개구리의 위치가 달라도 feature map에서 동일한 패턴이 이동한 위치에 그대로 나타난다.
-
CS231n 4강. Neural Networks and Backpropagation
Youtube: Stanford CS231N 2025
강의자료
1. Activation Function
선형 연산만으로 신경망을 구성하면 여러 개의 선형층을 쌓더라도 결국 하나의 선형층과 동일하게 표현된다. 이는 기존에 Linear Classifier와 마찬가지로 선형적으로 구분 가능한 문제만 해결할 수 있으며, 복잡한 비선형 관계를 학습할 수 없다.
이 한계를 극복하기 위해 활성화 함수(Activation Function) 를 사용한다. 활성화 함수는 각 층의 출력에 비선형성을 추가해주며, 일반적으로 신경망은 선형 층과 활성화 함수를 번갈아 쌓는 구조로 이루어진다.
또한, 한 층의 모든 뉴런이 다음 층의 모든 뉴런과 연결된 구조를 Fully Connected Layer(Dense Layer) 라고 한다.
대표적인 활성화 함수로는 ReLU, Sigmoid, tanh 등이 있으며, 각 함수의 세부적인 특징과 장단점은 이후 강의에서 다뤄질 예정이다.
2. Backpropagation
이전 강의에서 다룬 optimizer가 loss의 gradient를 이용해 파라미터를 업데이트 하는 역할을 했다면, 역전파(Backpropagation)는 딥러닝에서 각 파라미터에 대한 gradient를 실제로 계산하는 표준적인 방법이다.
역전파에 대해 본격적으로 다루기 전에, 강의에서는 신경망 구조를 계산 그래프(Computational Graph) 형태로 나타낸다.
Computaional Graph
Computational Graph는 계산의 구조를 node와 edge로 나타낸 것으로, 노드에는 연산을 표기하고 엣지에는 연산 결과를 화살표와 함께 표시한다.
이로써 전체 연산 구조를 시각화할 수 있음과 동시에, 특정 부분의 지역적인 계산에만 집중할 수도 있다.
역전파의 원리
우리가 역전파를 하는 목적은 Loss를 최소화 시키기 위해, 모델의 파라미터(가중치W나 편향b)를 어느 방향으로 얼마나 조정해야 하는지 알아내기 위함이다. 이를 식으로 나타내보면 Loss L에 대한 W의 편미분으로 나타낼 수 있다.
\[∂L/∂W\]
이때, 딥러닝 모델은 복합적인 여러 수학적 연산의 결합물이다. 단순히 L을 W에 대해 편미분하는 것은 간단하지만, 조정해야할 파라미터가 Loss에서 여러 단계의 연산을 거쳐야만 접근 가능한 경우도 있다. 이를 연쇄법칙(Chain Rule)을 통해 loss에서부터의 변화량을 모델의 모든 노드에까지 전파할 수 있다.
위의 슬라이드를 보면 초록색 숫자는 순전파 결과값이고, 빨간색 숫자가 각 노드의 gradient이다.
이때, 위의 계산 그래프를 보다시피 역방향으로 gradient를 구할때 전체 연산을 처음부터 끝까지 풀어서 미분할 필요가 없다. 자신의 상위 노드(upstream)에서 전달된 gradient에 자신의 연산에 대한 local gradient를 곱하기만 하면, 해당 노드의 gradient를 구할 수 있다.
지역 그래디언트(Local Gradient): 현재 노드의 연산에 대한 미분값
상류 그래디언트(Upstream Gradient): 현재 노드보다 출력(Loss) 쪽에 가까운, 이미 계산된 gradient
하류 그래디언트(Downstream Gradient): 현재 노드보다 입력 쪽에 가까운 노드의 gradient
Details
PyTorch와 같은 라이브러리의 autograd도 이와 동일한 방식으로 동작한다. 연산의 종류에 따라 gradient를 구하는 규칙이 미리 정의되어 있으니, 그 값을 downstream으로 계속 전달하고 누적하는 방식이다.
대표적인 두 연산을 예시로 보면:
덧셈 f(x) = c + x에서 x에 대한 미분은 1이다. 따라서 downstream으로는 1 × upstream gradient, 즉 upstream gradient를 그대로 전달한다.
곱셈 f = a × b에서 ∂f/∂a = b, ∂f/∂b = a이다. 따라서 각각 b × upstream gradient, a × upstream gradient를 downstream으로 전달한다.
3. Backpropagation in Tensor
앞서 원리 설명을 위해 역전파 대상이 scalar였다면, 실제 파라미터는 vector, matrix와 같은 n-dimensional array일 것이다. 기본적인 원리는 동일하나, scalar에서의 미분 대신 야코비안 행렬(Jacobian Matrix) 이 local gradient로 등장한다.
야코비안 행렬 J의 (i, j) 원소는 ∂y_i / ∂x_j로 정의된다. 그런데 ReLU와 같은 원소별 연산(element-wise operation) 에서는 출력 y_i가 오직 입력 x_i에만 의존하므로 i ≠ j인 경우 ∂y_i / ∂x_j = 0이 된다. 즉 야코비안이 대각 행렬(diagonal matrix) 이 되어 대부분의 원소가 0인 sparse matrix가 된다.
파라미터 수가 많아질수록 야코비안 행렬의 크기도 급격히 커지기 때문에, 이를 명시적으로 구성하는 것은 메모리와 연산 측면에서 매우 비효율적이다. 따라서 실제로는 야코비안 행렬을 직접 만들지 않고, upstream gradient와의 곱 결과만 원소별 곱셈으로 바로 계산하는 방식을 사용한다.
Backprop with Matrices
행렬 곱셈 y = xw (x: [N×D], w: [D×M], y: [N×M])의 역전파를 살펴보면, upstream gradient dL/dy의 shape은 y와 동일하게 [N×M]이다.
각 파라미터에 대한 gradient는 다음과 같이 계산된다:
\[\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} w^T \quad \text{([N×D] = [N×M] × [M×D])}\]
\[\frac{\partial L}{\partial w} = x^T \frac{\partial L}{\partial y} \quad \text{([D×M] = [D×N] × [N×M])}\]
이 공식은 외울 필요 없이, 결과의 shape이 원래 파라미터의 shape과 일치하도록 행렬의 순서와 전치(transpose)를 맞추면 자연스럽게 유도된다. “the only way to make shapes match up!”
-
CS231n 3강. Regularization and Optimization
Youtube: Stanford CS231N 2025
강의자료
1. Regularization
정규화 항은 손실함수에 추가되어 data loss이외에도 패널티를 부여해 모델이 Training Data에 과적합되는 것을 방지한다.
아래와 같이 학습된 모델 f1, f2가 있을때, f1은 모든 데이터의 지점을 통과하여 Training Loss는 f2보다 낮겠지만, 새로운 데이터 (Test Data)에 대한 Loss는 데이터의 추세를 더 잘 반영하는 f2가 더 낮을 것을 알 수 있다.
이처럼 Training loss는 f1이 더 작았지만 f2가 더 학습이 잘 된 모델이라고 할 수 있다. 이렇게 모델이 적당하게 학습할 수 있도록 패널티를 부여하는 것이 Regularization이다.
+) 오컴의 면도날(Occam’s Razor): ‘같은 현상을 설명하는 두 개의 주장이 있다면, 간단한 쪽을 선택하라,
1.1 L2 Regularization
L2 Regularization은 모든 가중치의 제곱합을 손실함수에 추가한다.
제곱을 하기 때문에 가중치가 클수록 패널티가 크게 작용하여 가중치를 빠르게 감소시키는 weight decay효과가 있다. 반대로 이미 값이 작은 가중치는 제곱하면 더 작아져 수치에 거의 영향을 주지 않는다.
이러한 특성으로 인해 L2는 가중치를 전체적으로 작지만 고르고 분산된(spread out) 형태로 만드는 경향이 있다.
전체 Loss는 다음과 같다:
\[L_{total} = L_{data} + \frac{\lambda}{2} W^2\]
여기서 1/2이 붙는 이유는 미분을 깔끔하게 하기 위한 수학적 편의이다.
\[\frac{\lambda}{2} W^2 \quad \xrightarrow{\text{미분}} \quad \frac{\lambda}{2} \cdot 2W = \lambda W\]
1.2 L1 Regularization
L1은 가중치를 제곱하는 대신 절댓값을 취해 더한 값을 사용한다.
제곱을 하지 않기 때문에 작은 가중치의 값을 0이 되게 만들어 해당 feature를 모델에서 제거하는 효과가 있다. 이는 모델을 더 단순하고 희소한(Sparse) 형태로 만든다.
\[L_{total} = L_{data} + \lambda \sum |W|\]
또한 L1, L2 공통적으로 조율모수 λ가 붙는데, 이 값 또한 커질수록 패널티의 영향이 커지며, 분산(Variance)은 감소하고 편향(Bias)이 증가한다.
아래와 같이 x=[1,1,1,1]; w1=[1,0,0,0]; w2=[0.25,0.25,0.25,0.25]일 때, w · x는 모두 1이지만 L2 Regularization을 할 경우 w2가 더 선호하는 가중치가 될 것이다.
L1 Regularization의 경우 이 예제에서는 w1, w2 둘 다 동일한 정규화 값을 가지지만 L1은 w1처럼 Sparse한 형태의 가중치를 선호할 것이다.
항상 어떤 정규화를 옳다거나, 정규화가 무조건 모델을 단순하게 만든다는 원칙은 없으며, 데이터와 모델에 따라 자신이 적절한 정규화 방법을 선택해야 한다.
1.3 ElasticNet
ElasticNet은 이 L1과 L2를 복합적으로 활용하는 정규화 기법이다.
하이퍼파라미터 r을 활용하여 각 기법의 영향도를 조절할 수 있으며, 한가지 기법만을 활용하는 것 또한 가능하다.
\[L_{total} = L_{data} + \lambda \left( r \sum |W| + \frac{(1-r)}{2} \sum W^2 \right)\]
2. Optimization
손실 함수의 값을 최소화하는 모델의 가중치를 찾는 과정이 바로 Optimization(최적화)이다.
이 최적화의 가장 기본이 되는 알고리즘은 Gradient Descent(경사하강법)이다. 손실함수의 경사(기울기)를 따라 내려가면서 가중치를 업데이트하여 손실함수가 가장 작아지는 지점을 찾는 방법이다.
\[W ← W − η · ∇_W L\]
이 수식은 Gradient Descent의 가중치 업데이트 수식이다.
기호
의미
W (우변)
현재 가중치
η (eta)
Learning Rate (학습률) — 한 번에 얼마나 이동할지 결정하는 하이퍼파라미터
∇_W L
손실 함수 L을 가중치 W로 편미분한 값 (Gradient)
W (좌변)
업데이트된 새로운 가중치
Gradient(∇_W L) 는 수학적 정의상 함수값이 가장 가파르게 증가하는 방향과 그 크기를 나타내는 벡터이다.
우리의 목표는 Loss를 최소화하는 것이므로, Gradient의 반대 방향(−) 으로 W를 조정하는 것이다.
딥러닝의 최적화에서는 거의 대부분 이러한 Analytic Gradient를 계산하여 최적화를 진행하고, Numeric Gradient는 구현이 올바른지 디버깅하는 용도로만 활용된다.
Numeric Gradient
Analytic Gradient
방법
h를 아주 작게 설정하여 (f(x+h) - f(x)) / h 로 근사 계산
미적분을 이용해 Gradient를 수식으로 직접 유도
정확도
근사값 (부정확)
정확한 값
속도
매우 느림
빠름
사용 목적
Gradient 구현이 올바른지 검증할 때 (Gradient Check)
실제 학습에 사용
3. Variant of Gradient Descent
3.1. Stochastic Gradient Descent (SGD)
기본적인 경사하강법은 전체 데이터에 대한 Loss와 Gradient를 계산하고 나서야 1회의 가중치 업데이트가 진행이 되었다. 이 방식은 데이터의 크기가 커질 경우 학습 속도가 매우 느릴 것이다.
이를 해결하기 위해 SGD에서는 전체 데이터에서 무작위로 일부 데이터를 뽑은 mini-batch에서 그래디언트를 계산해 W를 여러번 업데이트한다.
원래 SGD는 데이터를 1개씩 뽑아 업데이트 하는 것을 의미하지만, 실제로는 Mini-batch SGD를 통칭하여 SGD라고 부르는 경우가 대부분이다.
Epoch
전체 데이터에 대해 한 번 학습을 완료하는 시점
Batch Size
한 배치에 있는 데이터의 수
Iteration
한 Epoch를 완료하기 위해 반복하는 횟수 (한 Epoch에서 이루어지는 파라미터 업데이트 횟수)
GD
SGD
Mini-batch SGD
Batch Size
전체 데이터 (N)
1
일부 (32, 64, 128)
1 Epoch 당 Iteration
1
N
N / Batch Size
학습 속도
느림
빠르지만 불안정
빠르고 안정적
SGD의 문제점
1. Loss Surface의 경우에 따라 진동이 발생함
손실 함수가 어떤 방향으로는 매우 가파르고, 다른 방향으로는 매우 완만한 형태를 가지는 경우, 우리가 실제로 가고 싶은 곳은 완만한 방향의 끝에 있는 최솟값인데, 가파른 방향의 진동 때문에 지그재그로 비효율적으로 이동하게 된다.
좁고 긴 협곡의 바닥으로 내려가야 하는 상황을 비유하면, 협곡의 양 벽면(가파른 방향) 사이에서 왔다갔다 튕기거나 바깥으로 벗어날 수 있으며, 실제로 가야 할 아래쪽(완만한 방향)으로는 아주 조금씩만 전진하게 된다.
2. 지역 최솟값(Local Minimum) / 안장점(Saddle Point)에 갇힘
손실 함수의 Gradient가 0이 되는 지점에 도달하면 W 업데이트가 멈추게 된다. 이 지점이 전역 최솟값(Global Minimum)이 아니더라도 Gradient가 0이므로 더 이상 탈출할 수 없다.
3. Mini-batch 샘플링에 의한 Gradient 노이즈
모든 데이터가 아닌 일부 샘플링된 데이터를 기반으로 Gradient를 계산하기 때문에 전체 데이터에 대한 손실함수의 그래디언트를 정확히 반영하지는 못한다. 장기적으로는 Loss의 최소 지점을 찾아갈 수 있으나, 매 Step이 항상 올바른 방향을 보장하지는 않는다.
3.2. SGD + Momentum
기존 파라미터가 업데이트되어 온 방향을 기억하여 다음 업데이트에 반영하는 방식이다. 마치 물체가 움직이던 방향으로 관성을 가지는 것과 같다.
아래 식과 코드처럼 감쇠 계수 ρ(rho)가 곱해지며 누적된 Gradient(Vt)를 이용해 파라미터를 업데이트한다.
이로써 gradient=0이 되더라도 누적된 속도(Vt)가 남아있어 빠져나올 수 있고, 여전히 진동 문제는 남아있으나 최솟값을 향하는 방향의 Gradient가 지속적으로 누적되면서 점차 올바른 방향으로의 이동이 강화된다.
3.3. RMSProp
RMSProp은 Gradient의 크기를 추적하여 얼만큼 다음 step의 이동량을 조절하는 알고리즘이다.
Momentum에서는 Gradient의 방향과 크기가 함께 누적되었다면, RMSProp에서는 Gradient를 제곱하여 각 방향이 얼마나 가파른지 크기만을 추적한다. 이 누적된 제곱값으로 파라미터 업데이트 시 Learning Rate를 나눠줌으로써, 가파를수록 적게 움직이고, 완만한 구간에서는 크게 움직이는 효과를 보여준다.
\[S_t = \rho \cdot S_{t-1} + (1 - \rho) \cdot (\nabla_W L)^2\]
\[W \leftarrow W - \eta \cdot \frac{\nabla_W L}{\sqrt{S_t} + \epsilon}\]
기호
의미
$S_t$
Gradient 제곱의 누적값 (이동 평균)
$\rho$
감쇠 계수 (보통 0.9 또는 0.99)
$(\nabla_W L)^2$
현재 Gradient의 제곱
$\epsilon$
0으로 나누는 것을 방지하기 위한 아주 작은 값 (예: $10^{-7}$)
사람이 경사가 심한 곳은 조심히 작은 보폭으로 가고, 완만한 평지는 빠르게 이동하는 것을 생각하면된다.
3.4 Adam
Momentum SGD와 RMSProp의 기법을 합친 알고리즘으로, 오늘날 가장 많이 사용되는 최적화 기법이다.
Momentum: Gradient의 방향을 누적하여 관성을 부여 (1차 모멘트)
RMSProp: Gradient의 제곱을 누적하여 방향별 Learning Rate를 조절 (2차 모멘트)
1차 모멘트 (Momentum)
\[m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot \nabla_W L\]
2차 모멘트 (RMSProp)
\[v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot (\nabla_W L)^2\]
Bias Correction
Momentum과 RMSProp의 계산식은 거의 동일한데, 학습 초기에는 누적된 값이 거의 없어 $m_t$, $v_t$ 모두 0에 가깝게 되고, 이로 인해 업데이트 시 비정상적으로 큰 Step을 뛰게 될 수 있다.
이를 보정하기 위해 $1 - \beta^t$로 나누어주는 Bias Correction을 적용한다. 학습 초기($t$가 작을 때)에는 $\beta^t$가 1에 가까워 보정 효과가 크고, 학습이 진행될수록 $\beta^t$가 0에 수렴하면서 보정의 영향이 사라진다.
\[\hat{m}_t = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t}\]
\[W \leftarrow W - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}\]
기호
의미
일반적인 기본값
$m_t$
Gradient의 이동 평균 (1차 모멘트)
-
$v_t$
Gradient 제곱의 이동 평균 (2차 모멘트)
-
$\beta_1$
1차 모멘트 감쇠 계수
0.9
$\beta_2$
2차 모멘트 감쇠 계수
0.999
$\eta$
Learning Rate
$10^{-3}$
$\epsilon$
0으로 나누는 것을 방지하기 위한 작은 값
$10^{-8}$
+) AdamW
기존 Adam에서 L2 Regularization을 적용하면, 정규화 항이 Gradient에 포함된 채로 1차/2차 모멘트 계산에 들어간다. 이렇게 되면 Adaptive Learning Rate(√v_t로 나누는 부분)가 Data Loss의 Gradient뿐만 아니라 Regularization 항까지 함께 스케일링해버린다.
AdamW는 Weight Decay를 Gradient 계산과 분리하여, 파라미터 업데이트 단계에서 직접 적용한다.
\[W \leftarrow W - \eta \cdot \left( \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda W \right)\]
(Weight Decay $\lambda W$를 업데이트 시 별도로 적용. $\lambda W$ 는 L2정규화 항의 미분 결과)
4. Learning Rate Scheduling
전체 학습 과정 동안 반드시 같은 Learning Rate를 유지할 필요는 없으며, 오히려 이를 조절하는 것이 좋은 방법이다. 보통 학습이 진행될수록 LR을 감소시켜 Loss의 최소 지점 근처에서 수렴할 수 있도록 한다.
4.1 Step Decay
고정된 학습 반복 횟수(Epoch)마다 Learning Rate를 일정 비율로 줄이는 방식이다. (ResNet 등에서 사용)
4.2 Cosine Decay
Learning Rate를 코사인 함수의 곡선을 따라 부드럽게 감소시키는 방식이다. 초반에는 천천히 감소하다가 중반에 빠르게 감소하고, 후반에 다시 천천히 감소하는 특성을 가진다.
4.3 Linear Decay
Learning Rate를 학습이 진행됨에 따라 선형적으로 균일하게 감소시키는 방식이다.
4.4 Linear Warmup
초기부터 큰 Learning Rate를 사용하면 학습이 불안정할 수 있다. Linear Warmup은 학습 초기에 LR을 0에서 시작하여 목표 LR까지 점점 증가시켜 초기 학습의 안정성을 확보한다.
-
CS231n 2강. Image Classification with Linear Classifiers
Youtube: Stanford CS231N 2025
강의자료
1. Image Classification: A Core Task in Computer Vision
인간의 시각적 인지 능력은 짧은 순간에도 사진 속 객체가 무엇인지 판단할 수 있다. 고양이와 개를 구분하는 일은 우리에게 무의식적으로도 가능한 쉬운 문제지만, 컴퓨터에게 이는 어려운 도전 과제이다.
먼저 컴퓨터가 이미지를 보는 방식을 알아야 한다.
컴퓨터에서 이미지는 각 픽셀 값의 행렬 또는 텐서로 인식되며, 각 픽셀이 0~255값을 가지는 총 3개의 채널 (R/G/B)로 구성된다. 이처럼 이미 인간의 이미지 인식 방식과 매우 다르기 때문에, 컴퓨터의 이미지 분류에는 다양한 난관이 존재한다.
컴퓨터의 이미지 이해의 난관들
문제
설명
Viewpoint Variation
같은 물체도 촬영 각도에 따라 픽셀 값이 완전히 달라짐
Illumination
조명의 강도나 방향에 따라 동일 객체가 다르게 표현됨
Background Clutter
객체와 배경의 픽셀 값이 유사하면 구분이 어려움
Occlusion
객체 일부가 가려져 있어도 전체를 추론할 수 있어야 함
Deformation
같은 클래스의 객체라도 다양한 형태로 존재함
Intraclass Variation
같은 클래스 내에서도 크기, 색상, 패턴이 다양함
Context
주변 환경, 맥락을 이해해야 올바르게 분류 가능
1. Viewpoint Variation
같은 물체를 찍었더라도 찍은 위치에 따라 그 이미지 픽셀은 완전히 변화된 값을 가지게 된다.
2. Illumination
같은 객체여도 빛의 강도에 따라 이미지에는 다르게 표현된다. 우리는 조명의 차이를 바로 인지할 수 있으나 컴퓨터에겐 이것 또한 어려운 과제이다.
3. Background Clutter
인식하고자 하는 객체가 이미지의 배경과 비슷한 픽셀 값을 가지면 컴퓨터는 구분하기가 힘들어진다.
4. Occlusion
사진과 같이 동물의 일부가 다른 것에 가려져 있을 경우, 일부를 보고 물체를 추론할 수 있어야 한다.
5. Deformation
같은 클래스의 객체여도 다양한 형태로 존재 가능하다.
6. Intraclass variation
같은 클래스의 객체여도 다양한 종류가 있다는 것을 인지해야 한다. (size, color, pattern 이 전부 달라도 모두 cat이다)
7. Context
그림과 같이 우리는 그림자로 인해 개에게 줄무늬 그림자가 생긴 것을 이해할 수 있으나, 컴퓨터는 이를 호랑이로 분류할 수도 있을 것이다. 이처럼 이미지 속 상황에 대한 이해를 하는 능력이 요구된다.
2. K-Nearest Neighbor (KNN) Classifier
KNN은 가장 단순한 데이터 기반 접근법(Data-Driven Approach)의 분류기이다.
분류하려는 이미지와 학습으로 저장된 이미지들 간의 거리(픽셀값의 차이)를 계산하여, 가장 가까운 k개의 이미지를 찾고, 그 중 가장 많은 클래스로 분류한다.
이때 이미지들간의 거리를 측정하는데 다음과 같은 방식이 있다.
L1 Distance (맨해튼 거리): 각 픽셀 값의 차이를 절댓값하여 전부 더한 값.
L2 Distance (유클리드 거리): 각 픽셀 값의 차이를 제곱한 것을 전부 더한 후 제곱근. (일반적인 두 점 사이의 직선 거리)
\[d_1(I_1, I_2) = \sum_p |I_1^p - I_2^p|\]
\[d_2(I_1, I_2) = \sqrt{\sum_p (I_1^p - I_2^p)^2}\]
학습 방식: Lazy Learning
def train(self, X, y):
self.Xtr = X # 전체 학습 데이터를 그대로 저장
self.ytr = y
코드와 같이 train함수에서 그냥 전체 데이터셋을 메모리에 그대로 복사하고 있다. (주석 확인)
이처럼 일반적인 모델처럼 “학습(가중치 업데이트)”을 하는 게 아니라 훈련 데이터를 통째로 외워버리는 것이 KNN의 특징이며, 이를 “게으른 학습(Lazy Learning)” 이라고도 한다.
하지만 이러한 방식으로 인해 한계가 명확하다.
예측 속도: 학습은 빠르지만, 예측 시 전체 데이터와 거리를 계산해야 하므로 매우 느림
낮은 표현력: 픽셀 간 거리는 이미지의 의미적 정보를 담지 못함(Not informative). 이미지를 한 픽셀씩 밀거나, 가리거나, 밝기를 바꾸면 거리값이 크게 변해버림
3. Linear Classifier
파라미터 기반 접근법 (Parametric Approach)의 분류기로, 학습할 때 데이터를 단순히 저장하지 않고, 원하는 결과를 얻기 위해 파라미터(W)에 데이터로부터 얻은 정보를 저장, 최적화 하는 방식이다.
\[f(x, W) = Wx + b\]
$x$: 입력 이미지 (flatten된 벡터, 예: 32×32×3 → 3072차원)
$W$: 가중치 행렬 (학습으로 최적화할 파라미터)
$b$: 편향(Bias)
출력: 각 클래스에 대한 점수(Score)
선형 분류기는 f(x,W) = Wx + b로 나타낼 수 있으며 여기서 x는 입력 이미지로, 보통 선형 분류기에 들어올때 flatten된 상태로 들어온다. (Array of 32x32x3 ->3072 numbers total)
슬라이드의 예시처럼 입력데이터와 W가 곱해지는데, W의 각 행이 해당 클래스의 정보를 담고 있어 결과 값이 그 클래스에 대한 점수를 나타내게 된다.
Visual Viewpoint
W의 각 행을 이미지처럼 2차원의 이미지로 시각화하면, 각 클래스에 대한 특징이 추출된 형상을 확인할 수 있다.
Geometric Viewpoint
$Wx + b$를 공간에 나타내보면, 해당 클래스와 아닌 것을 분리하는 초평면(Hyperplane) 의 법선 벡터 역할을 한다.
하지만 이처럼 선형 분류기는 선형적으로 분리 가능하지 않으면 제대로 클래스를 분류할 수 없다는 치명적인 단점이 있다.
4. Loss Function & Softmax Loss
손실 함수는 모델의 예측이 실제 정답에 비해 얼마나 잘못 되었는지를 측정하는 함수이다. 모델을 학습시킨다는 것은 이 손실함수 값을 최소화하도록 모델의 파라미터를 조정하는 것이다.
Softmax
선형 분류기의 출력인 점수(logit) 그 자체로는 확률적 해석이 불가능하다.
Softmax함수는 여러 개의 출력값을 모두 합쳐서 1(100%)이 되는 확률 분포로 변환해준다. 이로써 분류기의 예측값을 각 클래스에 대한 예측 확률로 변환할 수 있다.
\[\text{softmax}(z_i) = \frac{e^{z_i}}{\sum_j e^{z_j}}\]
아래의 공식은 softmax의 개선된 공식으로, 지수에 입력 z의 최댓값을 빼줌으로써 수치 안정성을 확보한다.
\[\text{softmax}(z_i) = \frac{e^{z_i - \max(z)}}{\sum_j e^{z_j - \max(z)}}\]
Cross-Entropy Loss
확률 분포와 실제 정답(One-hot 벡터)과의 차이를 측정하는 손실 함수이다.
“모델이 예측한 확률 분포가 정답 분포와 얼마나 멀리 있는가?”
\[L = -\sum_{c} y_c \log(\hat{p}_c)\]
$y_c$: 정답 레이블 (정답 클래스는 1, 나머지는 0인 One-hot 벡터)
$\hat{p}_c$: 클래스 $c$의 예측 확률
정답 클래스를 제외한 나머지 $y_c$는 모두 0이므로, 결국 정답 클래스의 예측 확률만 남는다.
만약 정답으로 예측한 클래스의 확률이 1 이라면 손실함수의 값이 0이므로 최소이고, 정답 클래스의 예측확률이 작을 수록 앞에 - 부호로 인해 점점 손실함수의 값은 커지는 식이다.
\[L = -\log(\hat{p}_{y_i})\]
Softmax Loss
Softmax Loss는 Softmax 함수 + Cross-Entropy Loss 를 결합한 것으로, 정답 클래스의 예측 확률에 음의 로그를 취한 형태이다.
\[L_i = -\log\left(\frac{e^{z_{y_i}}}{\sum_j e^{z_j}}\right)\]
-
ResNet - Residual Network
ResNet - Residual Network
“Deep Residual Learning for Image Recognition” 논문에서 발표된 딥러닝 모델로, 많은 레이어를 가지게 된 깊은 신경망을 효과적으로 학습시키기 위해 Residual Learning이라는 개념을 사용한 모델이다.
논문: Deep Residual Learning for Image Recognition
이후에 등장한 많은 모델들에 영향을 준 아키텍쳐로, 논문을 읽고 직접 구현해보며, 공부한 내용을 정리해보았다.
1. ResNet의 등장 배경
AlexNet, VGGNet과 같은 CNN 기반 모델이 이미지 분류에서 좋은 성능을 내던 시절, 모델의 깊이(number of stacked layers)가 깊어질수록 이미지의 특징을 더 잘 추출하여 더 좋은 성능을 보인다는 것이 확인되었다.
그렇다면 무조건 모델의 레이어를 더 쌓는 것만으로 성능 향상을 이룰 수 있는 것일까? 아래의 그림(Figure 1)은 단순히 모델을 더 깊게 설계한다고 해서 성능 향상은 이루어지지 않고, 오히려 error가 증가하여 성능이 저하된다는 점을 보여준다.
레이어를 더 쌓은 56-layer 모델이 20-layer 모델보다 오히려 training error와 test error 모두 높게 나타났다. 이는 overfitting의 문제가 아니라, 모델이 깊어질수록 optimize하기가 어려워지는 degradation 문제임을 의미한다.
Identity Mapping
이를 위한 기본 아이디어로 Identity Mapping이라는 개념이 등장한다.
얕은 모델이 깊은 모델보다 성능이 더 좋다면, 모델을 더 깊게 만들 때 얕은 모델이 내놓은 결과값을 최대한 유지하도록 레이어를 학습시키는 것이다.
즉 만약 어느 얕은 모델을 통과하여 x라는 결과 값이 있다면, 그 이후의 레이어를 통과한 결과 또한 x와 비슷한 값을 내놓는다면, 괜찮은 성능을 보이는 얕은 모델과 적어도 비슷하거나 더 좋은 성능을 내는 모델이 나올 것이다.
이를 수식을 통해 표현해보면, 레이어의 출력을 H(x)라 할 때 기존에는 H(x)=y, 즉 결과가 참 값이 되도록 학습 시켰다면, H(x)=x 가 되도록 학습을 시키는 것이다.
이때 H(x)는 그 레이어의 연산 f( )를 거친 결과물이므로, 레이어의 함수 f를 거쳤을 때 H(x)=f(x)=x가 되도록 학습시키는 것이다.
이러한 identity mapping개념은 ResNet 이전에도 제시되었으나, 실제로 f(x) = x로 학습시키는 것이 굉장히 어려웠다. 하지만 ResNet에서는 이 문제를 조금 다른 방향에서 접근한다.
2. Residual Learning
앞서 봤듯이, 우리가 원하는 것은 H(x) = x, 즉 레이어를 통과해도 값이 유지되는 identity mapping이다.
ResNet은 H(x)를 f(x) 그대로 사용하는 대신, 입력 x를 직접 더해준 H(x) = f(x) + x 로 재정의한다.
Identity Mapping의 목적에 따라 H(x)=x가 되는 것을 원하므로, 최종적으로 f(x)=H(x)-x=0, 즉 잔차(Residual)가 0이 되도록 학습시키는 것으로 목표가 바뀌었다.
이렇게 구조를 바꿈으로써 생기는 장점은 다음과 같다:
f(x)=x를 직접 학습하는 것보다 f(x) = 0을 학습하는 것이 훨씬 수월하다.
입력이 직접 다음 레이어로 전달되므로, 역전파 과정에서도 gradient가 Shortcut을 통해 직접 전달될 수 있다. 이로 인해 Gradient Vanishing 문제가 완화된다.
첫번째 장점에 대해 자세히 얘기하자면, 기존 방식은 레이어가 아무런 힌트 없이 처음부터 f(x) = x라는 답을 스스로 찾아야 했다. 반면 ResNet은 shortcut을 통해 x를 구조적으로 이미 더해주고 있기 때문에, 레이어는 “x에서 얼마나 더 조정해야 하는가”라는 작은 변화량만 학습하면 된다. 기준점이 주어진 상태에서 미세한 조정만 하면 되니 훨씬 수월한 것이다.
이렇게 하나 이상의 레이어를 건너 뛰어 입력 x를 출력에 더해주는 연결을 Skip Connection 혹은 Shortcut이라고 한다. 논문의 ResNet 기본 구조에서는 weight layer 2개를 건너뛰게 설계되었다. (ResNet-50 이상은 3개로 바뀐다.) 기본적으로 이러한 Skip Connection과정이 생겨도 추가되는 모델의 파라미터가 전혀 없으며, dimension이 달라지는 경우에만 소량의 파라미터가 추가된다. (이후 Shorcut methods내용)
또한 이렇게 Skip Connection과 weight Layer, ReLU활성화 함수가 합쳐진 이 블록을 Residual Block이라고 부른다. ResNet은 이러한 Residual Block이 쌓여서 만들어지는 모델이다.
3. Network Architectures
왼쪽은 Skip Connection이 없는 Plain Network, 오른쪽이 Residual Block으로 구성된 ResNet이다.
Skip Connection(Shortcut)의 유무 이외에 다른 설계 원칙은 다음과 같이 동일하다:
VGGNet에서 영감을 받은 구조로, 3x3 kernel(filter)을 사용한다.
Skip Connection은 input과 output의 형태가 동일해야 하기에, 합성곱 연산 전후 특성맵의 shape과 filter의 갯수가 유지되도록 한다.
합성곱 연산에서 stride=2를 통해 특성 맵의 크기가 절반으로 줄어들 경우, 연산량을 일정하게 보존하기 위해 filter의 수 또한 2배로 늘린다.
3에서 특성맵의 크기가 줄어들면 skip connection을 그대로 적용할 수 없다. 이를 위해 shape을 맞추는 과정이 동반된다. (점선 화살표 부분) 아래의 내용에서 자세히 설명한다.
Shortcut methods
a) shortcut의 채널 수를 맞추기 위해 zero-padding을 추가하고, feature map 크기를
맞추기 위해 stride=2를 적용하여 더해준다.
기존에 x가 (28×28×64) (64개의 채널)이었다면 출력은 (14×14×128) (128개의 채널) 이어야 하므로, 부족한 64개 채널을 값이 전부 0인 feature map으로 채워서 128개를 맞춰주는 것이다.
이 방법은 추가되는 파라미터가 없다는 장점이 있으나, 0으로 채운 채널은 학습된 정보가 없다는 한계가 존재한다.
b) 1×1 conv(stride=2)를 shortcut에 적용하여 feature map의 크기와 채널 수를 출력과
동일하게 맞춰준 뒤 더해준다.
Ws가 바로 정사영(projection)에 해당하는 1×1 conv 레이어이다.
입력 x의 shape이 (28×28×64)이고 출력이 (14×14×128)이라면, 둘을 그대로 더할 수 없으니까 x에 1×1 conv를 stride=2로 적용해서 shape을 (14×14×128)로 맞춰준다.
이 방법은 학습해야하는 파라미터 수가 약간 추가된다. 본 구현과 ImageNet 실험에서는 b) 방법을 사용한다.
ResNet은 레이어의 깊이에 따라 ResNet-18, 34, 50, 101, 152 등 다양한 종류로 나뉘며, 그 구조는 아래 Table과 같다.
Bottleneck Block
위의 Table을 보면 ResNet 34-layer까지는 한 Residual Block에 3x3 Convolution 레이어가 2개 있는 구조였다면, 50-layer부터는 1x1, 3x3, 1x1 Convolution으로 총 3개의 레이어가 있는 구조인 것을 확인할 수 있다.
같은 구조의 block을 계속해서 쌓을 경우 파라미터의 수가 매우 많아지기 때문에, 연산량과 파라미터 수를 줄이면서도 층을 더 깊게 쌓기 위해 도입된 구조가 Bottleneck 구조이다.
1×1 Conv (차원 축소): 입력 레이어의 채널 수를 크게 줄임. (예: 256 → 64)
3×3 Conv (특징 추출): 채널 수가 줄어든 상태에서 공간적 특징을 추출.
1×1 Conv (차원 복원): 채널 수를 확장하여 Residual Connection과 크기를 맞춤 (예: 64 → 256)
4. PyTorch 구현 코드
PyTorch의 torchvision에 있는 ResNet 구현 코드를 기반으로 작성하였다.
원본 코드 링크(github)
Convolution layer
기본적인 convolution layer를 미리 정의한다.
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1):
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
BasicBlock
기본적인 Residual Block구조이다. forward()를 보면 F(x) + x가 마지막에 활성화 함수를 통과하는 ResNet v1의 방식이다.
\[H(x) = F(x) + x\]
\[y = \mathrm{ReLU}(H(x))\]
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(BasicBlock, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
Bottleneck
해당 원본 코드의 주석을 보면 stride=2의 conv연산을 3x3레이어에서 수행한다고 쓰여있는데, 이 말은 논문에서 첫 1x1 Conv에서 stride=2를 적용해 공간정보를 압축하던 것을 3x3으로 옮겼다는 의미이다.
이 구조가 더 정확성이 높아 개선된 구조이다.
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
ResNet
위에서 정의한 BasicBlock 또는 Bottleneck Block을 이용하여 전체 네트워크를 구성하는 클래스이다.
입력 이미지에 대해 초기 Convolution과 MaxPooling을 수행한 후, 4개의 Residual Layer를 통과시켜 특징을 추출한다. 이후 Average Pooling을 통해 공간 정보를 1×1 크기로 압축하고, FCN을 통과하여 최종 분류를 수행한다.
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
replace_stride_with_dilation = [False, False, False]
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))
return nn.Sequential(*layers)
def _forward_impl(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
self.inplanes
self.inplanes는 현재 Layer의 입력 채널 수를 의미한다. 첫 번째 Convolution의 출력 채널 수가 64이므로 초기값은 64로 설정된다.
이후 _make_layer()에서 첫 번째 Block이 생성된 후
self.inplanes = planes * block.expansion
을 통해 다음 Layer의 입력 채널 수로 갱신된다.
예를 들어 Bottleneck의 경우 expansion = 4 이므로 다음과 같이 채널 수가 증가한다.
64 → 256
128 → 512
256 → 1024
512 → 2048
_make_layer()
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
이 함수는 여러 개의 Residual Block을 묶어 하나의 Layer를 생성한다. 예를 들어 ResNet50에서는
layers = [3, 4, 6, 3]
이 전달되므로 아래와 같이 레이어가 생성된다.
Layer1 : Bottleneck 3개
Layer2 : Bottleneck 4개
Layer3 : Bottleneck 6개
Layer4 : Bottleneck 3개
ResNet50의 특성맵 변화
Stage
Output Shape
Input
3 × 224 × 224
Conv1
64 × 112 × 112
MaxPool
64 × 56 × 56
Layer1
256 × 56 × 56
Layer2
512 × 28 × 28
Layer3
1024 × 14 × 14
Layer4
2048 × 7 × 7
AvgPool
2048 × 1 × 1
FC
1000
정리
ResNet은 Skip Connection을 통해 Residual Learning을 수행해 깊은 신경망에서도 안정적인 학습이 가능하도록 만든 모델이다. Identity Mapping을 직접 학습하는 대신 Residual을 학습하도록 설계하여 Degradation 문제를 해결했으며, 이후 다양한 후속 연구의 기반이 되었다.